Closes #575 - Added `SupportDataGenerator` object which generates data sent with support email. Format of the data is based on what Android has. - Support button is moved to settings from profile. - Support button opens mail composer.
This commit is contained in:
parent
a4d35b759f
commit
2a560dea8d
|
@ -320,6 +320,16 @@
|
|||
3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3128E47666006ADEDB /* NotEnoughFreeSpaceView.swift */; };
|
||||
3448CB3728E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3628E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift */; };
|
||||
346715A528E2027D0035F7C4 /* CheckCircleStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346715A428E2027D0035F7C4 /* CheckCircleStore.swift */; };
|
||||
3467319529AE265300974482 /* SupportDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319429AE265300974482 /* SupportDataGenerator.swift */; };
|
||||
3467319629AE265300974482 /* SupportDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319429AE265300974482 /* SupportDataGenerator.swift */; };
|
||||
3467319929AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319829AE374300974482 /* SupportDataGeneratorInterface.swift */; };
|
||||
3467319A29AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319829AE374300974482 /* SupportDataGeneratorInterface.swift */; };
|
||||
3467319C29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319B29AE374A00974482 /* SupportDataGeneratorLiveKey.swift */; };
|
||||
3467319D29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319B29AE374A00974482 /* SupportDataGeneratorLiveKey.swift */; };
|
||||
3467319F29AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319E29AE375000974482 /* SupportDataGeneratorTestKey.swift */; };
|
||||
346731A029AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3467319E29AE375000974482 /* SupportDataGeneratorTestKey.swift */; };
|
||||
346731A229AE3A5100974482 /* UIMailDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346731A129AE3A5100974482 /* UIMailDialog.swift */; };
|
||||
346731A329AE3A5100974482 /* UIMailDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346731A129AE3A5100974482 /* UIMailDialog.swift */; };
|
||||
3469F18229ACD70500A07146 /* OnboardingFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3469F18129ACD70500A07146 /* OnboardingFlowFeatureFlagTests.swift */; };
|
||||
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346D41E328DF0B8600963F36 /* CheckCircle.swift */; };
|
||||
34BF09092927C98000222134 /* Memo+toString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BF09082927C98000222134 /* Memo+toString.swift */; };
|
||||
|
@ -645,6 +655,11 @@
|
|||
3448CB3128E47666006ADEDB /* NotEnoughFreeSpaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEnoughFreeSpaceView.swift; sourceTree = "<group>"; };
|
||||
3448CB3628E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEnoughFeeSpaceSnapshots.swift; sourceTree = "<group>"; };
|
||||
346715A428E2027D0035F7C4 /* CheckCircleStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircleStore.swift; sourceTree = "<group>"; };
|
||||
3467319429AE265300974482 /* SupportDataGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportDataGenerator.swift; sourceTree = "<group>"; };
|
||||
3467319829AE374300974482 /* SupportDataGeneratorInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportDataGeneratorInterface.swift; sourceTree = "<group>"; };
|
||||
3467319B29AE374A00974482 /* SupportDataGeneratorLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportDataGeneratorLiveKey.swift; sourceTree = "<group>"; };
|
||||
3467319E29AE375000974482 /* SupportDataGeneratorTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportDataGeneratorTestKey.swift; sourceTree = "<group>"; };
|
||||
346731A129AE3A5100974482 /* UIMailDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMailDialog.swift; sourceTree = "<group>"; };
|
||||
3469F18129ACD70500A07146 /* OnboardingFlowFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowFeatureFlagTests.swift; sourceTree = "<group>"; };
|
||||
346D41E328DF0B8600963F36 /* CheckCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircle.swift; sourceTree = "<group>"; };
|
||||
34BF09082927C98000222134 /* Memo+toString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Memo+toString.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1173,6 +1188,17 @@
|
|||
path = SendSnapshotTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3467319729AE36F000974482 /* SupportDataGenerator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3467319429AE265300974482 /* SupportDataGenerator.swift */,
|
||||
3467319829AE374300974482 /* SupportDataGeneratorInterface.swift */,
|
||||
3467319B29AE374A00974482 /* SupportDataGeneratorLiveKey.swift */,
|
||||
3467319E29AE375000974482 /* SupportDataGeneratorTestKey.swift */,
|
||||
);
|
||||
path = SupportDataGenerator;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
346D41E228DF0B0900963F36 /* CheckCircle */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1464,6 +1490,7 @@
|
|||
9E612C6D2987A96500D09B09 /* UIKitBridge */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
346731A129AE3A5100974482 /* UIMailDialog.swift */,
|
||||
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */,
|
||||
);
|
||||
path = UIKitBridge;
|
||||
|
@ -1649,6 +1676,7 @@
|
|||
9EB863A329239D95003D0F8B /* RecoveryPhraseRandomizer */,
|
||||
9EB863B62923C539003D0F8B /* SDKSynchronizer */,
|
||||
9EB863B32923C465003D0F8B /* SecItem */,
|
||||
3467319729AE36F000974482 /* SupportDataGenerator */,
|
||||
9EB8639E29239891003D0F8B /* URIParser */,
|
||||
9E153A7129216EBD00112F41 /* UserDefaults */,
|
||||
9EB863B72923C55A003D0F8B /* UserPreferencesStorage */,
|
||||
|
@ -2557,6 +2585,7 @@
|
|||
0D26AEA5299E8196005260EE /* TransactionSendingView.swift in Sources */,
|
||||
0D26AEA6299E8196005260EE /* WalletEventsFlowView.swift in Sources */,
|
||||
0D26AEA7299E8196005260EE /* CaptureDeviceLiveKey.swift in Sources */,
|
||||
3467319A29AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */,
|
||||
0D26AEA8299E8196005260EE /* AudioServicesInterface.swift in Sources */,
|
||||
0D26AEA9299E8196005260EE /* NotificationCenterTest.swift in Sources */,
|
||||
0D26AEAA299E8196005260EE /* CrashReporterLiveKey.swift in Sources */,
|
||||
|
@ -2605,15 +2634,18 @@
|
|||
0D26AED3299E8196005260EE /* SyncStatusSnapshot.swift in Sources */,
|
||||
0D26AED4299E8196005260EE /* SecantButtonStyles.swift in Sources */,
|
||||
0D26AED5299E8196005260EE /* RecoveryPhraseBackupFailedView.swift in Sources */,
|
||||
346731A029AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */,
|
||||
0D26AED6299E8196005260EE /* UserPreferencesStorageInterface.swift in Sources */,
|
||||
0D26AED7299E8196005260EE /* DiskSpaceCheckerMocks.swift in Sources */,
|
||||
0D26AED8299E8196005260EE /* DropDelegate.swift in Sources */,
|
||||
0D26AED9299E8196005260EE /* LocalAuthenticationLiveKey.swift in Sources */,
|
||||
0D26AEDA299E8196005260EE /* ImportSeedEditor.swift in Sources */,
|
||||
0D26AEDB299E8196005260EE /* ProfileStore.swift in Sources */,
|
||||
3467319629AE265300974482 /* SupportDataGenerator.swift in Sources */,
|
||||
0D26AEDC299E8196005260EE /* CheckCircle.swift in Sources */,
|
||||
0D26AEDD299E8196005260EE /* LogStore.swift in Sources */,
|
||||
0D26AEDE299E8196005260EE /* RecoveryPhraseRandomizer.swift in Sources */,
|
||||
3467319D29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */,
|
||||
0D26AEDF299E8196005260EE /* FileManagerTestKey.swift in Sources */,
|
||||
0D26AEE0299E8196005260EE /* SecItemLive.swift in Sources */,
|
||||
0D26AEE1299E8196005260EE /* CircularFrameBadge.swift in Sources */,
|
||||
|
@ -2733,6 +2765,7 @@
|
|||
0D26AF52299E8196005260EE /* LogsHandlerTest.swift in Sources */,
|
||||
0D26AF53299E8196005260EE /* TextFieldFooter.swift in Sources */,
|
||||
0D26AF54299E8196005260EE /* CrashReportingInterface.swift in Sources */,
|
||||
346731A329AE3A5100974482 /* UIMailDialog.swift in Sources */,
|
||||
0D26AF55299E8196005260EE /* ProfileView.swift in Sources */,
|
||||
0D26AF56299E8196005260EE /* ScanStore.swift in Sources */,
|
||||
0D26AF57299E8196005260EE /* NumberFormatterTestKey.swift in Sources */,
|
||||
|
@ -2781,6 +2814,7 @@
|
|||
34DA414728E4385800F8CC61 /* TransactionSendingView.swift in Sources */,
|
||||
F96B41E9273B501F0021B49A /* WalletEventsFlowView.swift in Sources */,
|
||||
9EBDF96E291ECED4000A1A05 /* CaptureDeviceLiveKey.swift in Sources */,
|
||||
3467319929AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */,
|
||||
9EBDF968291ECDA2000A1A05 /* AudioServicesInterface.swift in Sources */,
|
||||
9EB863BD2923C704003D0F8B /* NotificationCenterTest.swift in Sources */,
|
||||
0D26103E298C3FA600CC9DE9 /* CrashReporterLiveKey.swift in Sources */,
|
||||
|
@ -2829,15 +2863,18 @@
|
|||
9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */,
|
||||
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */,
|
||||
0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */,
|
||||
3467319F29AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */,
|
||||
0D63170029919970007D873F /* UserPreferencesStorageInterface.swift in Sources */,
|
||||
9EBDF94D291D773A000A1A05 /* DiskSpaceCheckerMocks.swift in Sources */,
|
||||
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */,
|
||||
9EBDF986291F91EF000A1A05 /* LocalAuthenticationLiveKey.swift in Sources */,
|
||||
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
|
||||
F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */,
|
||||
3467319529AE265300974482 /* SupportDataGenerator.swift in Sources */,
|
||||
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */,
|
||||
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */,
|
||||
9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */,
|
||||
3467319C29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */,
|
||||
9EB863C52923C8AF003D0F8B /* FileManagerTestKey.swift in Sources */,
|
||||
9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */,
|
||||
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
|
||||
|
@ -2957,6 +2994,7 @@
|
|||
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
|
||||
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
|
||||
0D26103C298C3E4800CC9DE9 /* CrashReportingInterface.swift in Sources */,
|
||||
346731A229AE3A5100974482 /* UIMailDialog.swift in Sources */,
|
||||
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
|
||||
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
|
||||
9EB863952922D036003D0F8B /* NumberFormatterTestKey.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// SupportDataGenerator.swift
|
||||
// secant
|
||||
//
|
||||
// Created by Michal Fousek on 28.02.2023.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import UIKit
|
||||
|
||||
struct SupportData: Equatable {
|
||||
let toAddress: String
|
||||
let subject: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
enum SupportDataGenerator {
|
||||
static func generate() -> SupportData {
|
||||
let items: [SupportDataGeneratorItem] = [
|
||||
TimeItem(),
|
||||
AppVersionItem(),
|
||||
SystemVersionItem(),
|
||||
DeviceModelItem(),
|
||||
LocaleItem(),
|
||||
FreeDiskSpaceItem(),
|
||||
PermissionsItems()
|
||||
]
|
||||
|
||||
let message = items
|
||||
.map { $0.generate() }
|
||||
.flatMap { $0 }
|
||||
.map { "\($0.0): \($0.1)" }
|
||||
.joined(separator: "\n")
|
||||
|
||||
return SupportData(toAddress: "support@electriccoin.co", subject: "sECCant", message: message)
|
||||
}
|
||||
}
|
||||
|
||||
private protocol SupportDataGeneratorItem {
|
||||
func generate() -> [(String, String)]
|
||||
}
|
||||
|
||||
private struct TimeItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let timeKey = "Current time"
|
||||
}
|
||||
|
||||
let dateFormatter: DateFormatter
|
||||
|
||||
init() {
|
||||
dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss a ZZZZ"
|
||||
dateFormatter.locale = Locale(identifier: "en_US")
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
return [(Constants.timeKey, dateFormatter.string(from: Date()))]
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppVersionItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let bundleIdentifierKey = "App identifier"
|
||||
static let versionKey = "App version"
|
||||
static let unknownVersion = "Unknown"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
let bundle = Bundle.main
|
||||
guard let infoDict = bundle.infoDictionary else { return [(Constants.versionKey, Constants.unknownVersion)] }
|
||||
|
||||
var data: [(String, String)] = []
|
||||
if let bundleIdentifier = bundle.bundleIdentifier {
|
||||
data.append((Constants.bundleIdentifierKey, bundleIdentifier))
|
||||
}
|
||||
|
||||
if let build = infoDict["CFBundleVersion"] as? String, let version = infoDict["CFBundleShortVersionString"] as? String {
|
||||
data.append((Constants.versionKey, "\(version) (\(build))"))
|
||||
} else {
|
||||
data.append((Constants.versionKey, Constants.unknownVersion))
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
private struct SystemVersionItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let systemVersionKey = "iOS version"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
return [(Constants.systemVersionKey, UIDevice.current.systemVersion)]
|
||||
}
|
||||
}
|
||||
|
||||
private struct DeviceModelItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let deviceModelKey = "Device"
|
||||
static let unknownDevice = "unknown"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
var readModel: String?
|
||||
withUnsafePointer(to: &systemInfo.machine.0) { charPointer in
|
||||
readModel = String(cString: charPointer, encoding: .ascii)
|
||||
}
|
||||
|
||||
let model = readModel ?? Constants.unknownDevice
|
||||
return [(Constants.deviceModelKey, model)]
|
||||
}
|
||||
}
|
||||
|
||||
private struct LocaleItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let localKey = "Locale"
|
||||
static let groupingSeparatorKey = "Currency grouping separato"
|
||||
static let decimalSeparatorKey = "Currency decimal separator"
|
||||
static let unknownSeparator = "unknown"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
let locale = Locale.current
|
||||
|
||||
return [
|
||||
(Constants.localKey, locale.identifier),
|
||||
(Constants.groupingSeparatorKey, locale.groupingSeparator ?? Constants.unknownSeparator),
|
||||
(Constants.decimalSeparatorKey, locale.decimalSeparator ?? Constants.unknownSeparator)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private struct FreeDiskSpaceItem: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let freeDiskSpaceKey = "Usable storage"
|
||||
static let freeDiskSpaceUnknown = "unknown"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
let freeDiskSpace: String
|
||||
|
||||
let fileURL = URL(fileURLWithPath: NSHomeDirectory())
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
|
||||
if let freeSpace = values.volumeAvailableCapacityForImportantUsage {
|
||||
freeDiskSpace = "\(freeSpace / 1024 / 1024) MB"
|
||||
} else {
|
||||
freeDiskSpace = Constants.freeDiskSpaceUnknown
|
||||
}
|
||||
} catch {
|
||||
LoggerProxy.debug("Can't get free disk space: \(error)")
|
||||
freeDiskSpace = Constants.freeDiskSpaceUnknown
|
||||
}
|
||||
|
||||
return [(Constants.freeDiskSpaceKey, freeDiskSpace)]
|
||||
}
|
||||
}
|
||||
|
||||
private struct PermissionsItems: SupportDataGeneratorItem {
|
||||
private enum Constants {
|
||||
static let permissionsKey = "Permissions"
|
||||
static let cameraPermKey = "Camera access"
|
||||
static let faceIDAvailable = "FaceID available"
|
||||
static let touchIDAvailable = "TouchID available"
|
||||
static let yesText = "yes"
|
||||
static let noText = "no"
|
||||
}
|
||||
|
||||
func generate() -> [(String, String)] {
|
||||
let cameraAuthorized = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
||||
|
||||
let bioAuthContext = LAContext()
|
||||
let biometricAuthAvailable = bioAuthContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||
|
||||
return [
|
||||
(Constants.permissionsKey, ""),
|
||||
(Constants.cameraPermKey, cameraAuthorized ? Constants.yesText : Constants.noText),
|
||||
(Constants.faceIDAvailable, biometricAuthAvailable && bioAuthContext.biometryType == .faceID ? Constants.yesText : Constants.noText),
|
||||
(Constants.touchIDAvailable, biometricAuthAvailable && bioAuthContext.biometryType == .touchID ? Constants.yesText : Constants.noText)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// SupportDataGeneratorInterface.swift
|
||||
// secant
|
||||
//
|
||||
// Created by Michal Fousek on 28.02.2023.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
|
||||
extension DependencyValues {
|
||||
var supportDataGenerator: SupportDataGeneratorClient {
|
||||
get { self[SupportDataGeneratorClient.self] }
|
||||
set { self[SupportDataGeneratorClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportDataGeneratorClient {
|
||||
let generate: () -> SupportData
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// SupportDataGeneratorLiveKey.swift
|
||||
// secant
|
||||
//
|
||||
// Created by Michal Fousek on 28.02.2023.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
|
||||
extension SupportDataGeneratorClient: DependencyKey {
|
||||
static let liveValue = Self(
|
||||
generate: { SupportDataGenerator.generate() }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// SupportDataGeneratorTestKey.swift
|
||||
// secant
|
||||
//
|
||||
// Created by Michal Fousek on 28.02.2023.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
import XCTestDynamicOverlay
|
||||
|
||||
extension SupportDataGeneratorClient: TestDependencyKey {
|
||||
static let testValue = Self(
|
||||
generate: XCTUnimplemented("\(Self.self).generate")
|
||||
)
|
||||
}
|
||||
|
||||
extension SupportDataGeneratorClient {
|
||||
static let noOp = Self(
|
||||
generate: { SupportData(toAddress: "", subject: "", message: "") }
|
||||
)
|
||||
}
|
|
@ -37,14 +37,6 @@ struct ProfileView: View {
|
|||
.frame(height: 50)
|
||||
.padding(EdgeInsets(top: 30, leading: 30, bottom: 20, trailing: 30))
|
||||
|
||||
Button(
|
||||
action: { },
|
||||
label: { Text("Support") }
|
||||
)
|
||||
.primaryButtonStyle
|
||||
.frame(height: 50)
|
||||
.padding(EdgeInsets(top: 0, leading: 30, bottom: 20, trailing: 30))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ComposableArchitecture
|
||||
import MessageUI
|
||||
import SwiftUI
|
||||
|
||||
typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action>
|
||||
|
@ -10,13 +11,14 @@ struct SettingsReducer: ReducerProtocol {
|
|||
case backupPhrase
|
||||
}
|
||||
|
||||
@BindingState var alert: AlertState<SettingsReducer.Action>?
|
||||
var destination: Destination?
|
||||
var exportLogsDisabled = false
|
||||
@BindingState var isCrashReportingOn: Bool
|
||||
var isSharingLogs = false
|
||||
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
|
||||
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
|
||||
|
||||
@BindingState var isCrashReportingOn: Bool
|
||||
var supportData: SupportData?
|
||||
|
||||
var tempSDKDir: URL {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
|
@ -42,6 +44,7 @@ struct SettingsReducer: ReducerProtocol {
|
|||
case backupWalletAccessRequest
|
||||
case binding(BindingAction<SettingsReducer.State>)
|
||||
case cancelRescan
|
||||
case dismissAlert
|
||||
case exportLogs
|
||||
case fullRescan
|
||||
case logsExported
|
||||
|
@ -50,8 +53,10 @@ struct SettingsReducer: ReducerProtocol {
|
|||
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
|
||||
case quickRescan
|
||||
case rescanBlockchain
|
||||
case updateDestination(SettingsReducer.State.Destination?)
|
||||
case sendSupportMail
|
||||
case sendSupportMailFinished
|
||||
case testCrashReporter // this will crash the app if live.
|
||||
case updateDestination(SettingsReducer.State.Destination?)
|
||||
}
|
||||
|
||||
@Dependency(\.localAuthentication) var localAuthentication
|
||||
|
@ -102,6 +107,10 @@ struct SettingsReducer: ReducerProtocol {
|
|||
state.rescanDialog = nil
|
||||
return .none
|
||||
|
||||
case .dismissAlert:
|
||||
state.alert = nil
|
||||
return .none
|
||||
|
||||
case .exportLogs:
|
||||
state.exportLogsDisabled = true
|
||||
return .run { [state] send in
|
||||
|
@ -148,6 +157,26 @@ struct SettingsReducer: ReducerProtocol {
|
|||
|
||||
case .binding:
|
||||
return .none
|
||||
|
||||
case .sendSupportMail:
|
||||
if MFMailComposeViewController.canSendMail() {
|
||||
state.supportData = SupportDataGenerator.generate()
|
||||
} else {
|
||||
state.alert = AlertState(
|
||||
title: TextState("Can't send email"),
|
||||
message: TextState("""
|
||||
It looks like that you don't have any email account configured on your device. Therefore it's not possible to send a support \
|
||||
email.
|
||||
"""),
|
||||
dismissButton: .default(TextState("Ok"), action: .send(.sendSupportMailFinished))
|
||||
)
|
||||
}
|
||||
|
||||
return .none
|
||||
|
||||
case .sendSupportMailFinished:
|
||||
state.supportData = nil
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,10 +219,10 @@ extension SettingsStore {
|
|||
|
||||
extension SettingsReducer.State {
|
||||
static let placeholder = SettingsReducer.State(
|
||||
isCrashReportingOn: true,
|
||||
phraseDisplayState: RecoveryPhraseDisplayReducer.State(
|
||||
phrase: .placeholder
|
||||
),
|
||||
isCrashReportingOn: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,14 @@ struct SettingsView: View {
|
|||
)
|
||||
.primaryButtonStyle
|
||||
.frame(height: 50)
|
||||
|
||||
Button(
|
||||
action: { viewStore.send(.sendSupportMail) },
|
||||
label: { Text("Send us feedback!") }
|
||||
)
|
||||
.primaryButtonStyle
|
||||
.frame(height: 50)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
|
@ -64,6 +72,7 @@ struct SettingsView: View {
|
|||
}
|
||||
)
|
||||
.onAppear { viewStore.send(.onAppear) }
|
||||
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
|
||||
|
||||
if viewStore.isSharingLogs {
|
||||
UIShareDialogView(
|
||||
|
@ -75,6 +84,18 @@ struct SettingsView: View {
|
|||
// so frame is set to 0 to not break SwiftUIs layout
|
||||
.frame(width: 0, height: 0)
|
||||
}
|
||||
|
||||
if let supportData = viewStore.supportData {
|
||||
UIMailDialogView(
|
||||
supportData: supportData,
|
||||
completion: {
|
||||
viewStore.send(.sendSupportMailFinished)
|
||||
}
|
||||
)
|
||||
// UIMailDialogView only wraps MFMailComposeViewController presentation
|
||||
// so frame is set to 0 to not break SwiftUIs layout
|
||||
.frame(width: 0, height: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// UIMailDialog.swift
|
||||
// secant
|
||||
//
|
||||
// Created by Michal Fousek on 28.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MessageUI
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class UIMailDialog: UIView {
|
||||
var completion: (() -> Void)?
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIMailDialog {
|
||||
func doInitialSetup(supportData: SupportData, completion: @escaping () -> Void) {
|
||||
self.completion = completion
|
||||
DispatchQueue.main.async {
|
||||
let mailVC = MFMailComposeViewController()
|
||||
mailVC.mailComposeDelegate = self
|
||||
|
||||
// Configure the fields of the interface.
|
||||
mailVC.setToRecipients([supportData.toAddress])
|
||||
mailVC.setSubject(supportData.subject)
|
||||
mailVC.setMessageBody("\n\n\(supportData.message)", isHTML: false)
|
||||
|
||||
let rootVC = UIApplication.shared.connectedScenes
|
||||
.map { $0 as? UIWindowScene }
|
||||
.compactMap { $0 }
|
||||
.first?.windows.first?.rootViewController
|
||||
|
||||
rootVC?.present(
|
||||
mailVC,
|
||||
animated: true,
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIMailDialog: MFMailComposeViewControllerDelegate {
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
controller.dismiss(animated: true, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
struct UIMailDialogView: UIViewRepresentable {
|
||||
let supportData: SupportData
|
||||
let completion: () -> Void
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<UIMailDialogView>) -> UIMailDialog {
|
||||
let view = UIMailDialog()
|
||||
view.doInitialSetup(supportData: supportData, completion: completion)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIMailDialog, context: UIViewRepresentableContext<UIMailDialogView>) {
|
||||
// We can leave it empty here because the view is just handler how to bridge UIKit's UIActivityViewController
|
||||
// presentation into SwiftUI. The view itself is not visible, only instantiated, therefore no updates needed.
|
||||
}
|
||||
|
||||
typealias UIViewType = UIMailDialog
|
||||
}
|
|
@ -46,8 +46,8 @@ class SettingsTests: XCTestCase {
|
|||
|
||||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil),
|
||||
isCrashReportingOn: false
|
||||
isCrashReportingOn: false,
|
||||
phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
) { dependencies in
|
||||
|
@ -103,6 +103,7 @@ class SettingsTests: XCTestCase {
|
|||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
destination: nil,
|
||||
isCrashReportingOn: false,
|
||||
phraseDisplayState: .init(),
|
||||
rescanDialog: .init(
|
||||
title: TextState("Rescan"),
|
||||
|
@ -112,8 +113,7 @@ class SettingsTests: XCTestCase {
|
|||
.default(TextState("Full rescan"), action: .send(.fullRescan)),
|
||||
.cancel(TextState("Cancel"))
|
||||
]
|
||||
),
|
||||
isCrashReportingOn: false
|
||||
)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
)
|
||||
|
@ -127,6 +127,7 @@ class SettingsTests: XCTestCase {
|
|||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
destination: nil,
|
||||
isCrashReportingOn: false,
|
||||
phraseDisplayState: .init(),
|
||||
rescanDialog: .init(
|
||||
title: TextState("Rescan"),
|
||||
|
@ -136,8 +137,7 @@ class SettingsTests: XCTestCase {
|
|||
.default(TextState("Full rescan"), action: .send(.fullRescan)),
|
||||
.cancel(TextState("Cancel"))
|
||||
]
|
||||
),
|
||||
isCrashReportingOn: false
|
||||
)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
)
|
||||
|
@ -151,6 +151,7 @@ class SettingsTests: XCTestCase {
|
|||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
destination: nil,
|
||||
isCrashReportingOn: false,
|
||||
phraseDisplayState: .init(),
|
||||
rescanDialog: .init(
|
||||
title: TextState("Rescan"),
|
||||
|
@ -160,8 +161,7 @@ class SettingsTests: XCTestCase {
|
|||
.default(TextState("Full rescan"), action: .send(.fullRescan)),
|
||||
.cancel(TextState("Cancel"))
|
||||
]
|
||||
),
|
||||
isCrashReportingOn: false
|
||||
)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
)
|
||||
|
@ -175,6 +175,7 @@ class SettingsTests: XCTestCase {
|
|||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
destination: nil,
|
||||
isCrashReportingOn: false,
|
||||
phraseDisplayState: .init(),
|
||||
rescanDialog: .init(
|
||||
title: TextState("Rescan"),
|
||||
|
@ -184,8 +185,7 @@ class SettingsTests: XCTestCase {
|
|||
.default(TextState("Full rescan"), action: .send(.fullRescan)),
|
||||
.cancel(TextState("Cancel"))
|
||||
]
|
||||
),
|
||||
isCrashReportingOn: false
|
||||
)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
)
|
||||
|
@ -206,6 +206,7 @@ class SettingsTests: XCTestCase {
|
|||
let store = TestStore(
|
||||
initialState: SettingsReducer.State(
|
||||
destination: nil,
|
||||
isCrashReportingOn: false,
|
||||
isSharingLogs: true,
|
||||
phraseDisplayState: .init(),
|
||||
rescanDialog: .init(
|
||||
|
@ -216,8 +217,7 @@ class SettingsTests: XCTestCase {
|
|||
.default(TextState("Full rescan"), action: .send(.fullRescan)),
|
||||
.cancel(TextState("Cancel"))
|
||||
]
|
||||
),
|
||||
isCrashReportingOn: false
|
||||
)
|
||||
),
|
||||
reducer: SettingsReducer()
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue