[#575] Add support for sending feedback (#588)

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:
Michal Fousek 2023-03-01 09:52:50 +01:00 committed by GitHub
parent a4d35b759f
commit 2a560dea8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 418 additions and 25 deletions

View File

@ -320,6 +320,16 @@
3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3128E47666006ADEDB /* NotEnoughFreeSpaceView.swift */; }; 3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3128E47666006ADEDB /* NotEnoughFreeSpaceView.swift */; };
3448CB3728E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3628E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift */; }; 3448CB3728E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448CB3628E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift */; };
346715A528E2027D0035F7C4 /* CheckCircleStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346715A428E2027D0035F7C4 /* CheckCircleStore.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 */; }; 3469F18229ACD70500A07146 /* OnboardingFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3469F18129ACD70500A07146 /* OnboardingFlowFeatureFlagTests.swift */; };
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346D41E328DF0B8600963F36 /* CheckCircle.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 34BF09082927C98000222134 /* Memo+toString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Memo+toString.swift"; sourceTree = "<group>"; };
@ -1173,6 +1188,17 @@
path = SendSnapshotTests; path = SendSnapshotTests;
sourceTree = "<group>"; 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 */ = { 346D41E228DF0B0900963F36 /* CheckCircle */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1464,6 +1490,7 @@
9E612C6D2987A96500D09B09 /* UIKitBridge */ = { 9E612C6D2987A96500D09B09 /* UIKitBridge */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
346731A129AE3A5100974482 /* UIMailDialog.swift */,
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */, 9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */,
); );
path = UIKitBridge; path = UIKitBridge;
@ -1649,6 +1676,7 @@
9EB863A329239D95003D0F8B /* RecoveryPhraseRandomizer */, 9EB863A329239D95003D0F8B /* RecoveryPhraseRandomizer */,
9EB863B62923C539003D0F8B /* SDKSynchronizer */, 9EB863B62923C539003D0F8B /* SDKSynchronizer */,
9EB863B32923C465003D0F8B /* SecItem */, 9EB863B32923C465003D0F8B /* SecItem */,
3467319729AE36F000974482 /* SupportDataGenerator */,
9EB8639E29239891003D0F8B /* URIParser */, 9EB8639E29239891003D0F8B /* URIParser */,
9E153A7129216EBD00112F41 /* UserDefaults */, 9E153A7129216EBD00112F41 /* UserDefaults */,
9EB863B72923C55A003D0F8B /* UserPreferencesStorage */, 9EB863B72923C55A003D0F8B /* UserPreferencesStorage */,
@ -2557,6 +2585,7 @@
0D26AEA5299E8196005260EE /* TransactionSendingView.swift in Sources */, 0D26AEA5299E8196005260EE /* TransactionSendingView.swift in Sources */,
0D26AEA6299E8196005260EE /* WalletEventsFlowView.swift in Sources */, 0D26AEA6299E8196005260EE /* WalletEventsFlowView.swift in Sources */,
0D26AEA7299E8196005260EE /* CaptureDeviceLiveKey.swift in Sources */, 0D26AEA7299E8196005260EE /* CaptureDeviceLiveKey.swift in Sources */,
3467319A29AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */,
0D26AEA8299E8196005260EE /* AudioServicesInterface.swift in Sources */, 0D26AEA8299E8196005260EE /* AudioServicesInterface.swift in Sources */,
0D26AEA9299E8196005260EE /* NotificationCenterTest.swift in Sources */, 0D26AEA9299E8196005260EE /* NotificationCenterTest.swift in Sources */,
0D26AEAA299E8196005260EE /* CrashReporterLiveKey.swift in Sources */, 0D26AEAA299E8196005260EE /* CrashReporterLiveKey.swift in Sources */,
@ -2605,15 +2634,18 @@
0D26AED3299E8196005260EE /* SyncStatusSnapshot.swift in Sources */, 0D26AED3299E8196005260EE /* SyncStatusSnapshot.swift in Sources */,
0D26AED4299E8196005260EE /* SecantButtonStyles.swift in Sources */, 0D26AED4299E8196005260EE /* SecantButtonStyles.swift in Sources */,
0D26AED5299E8196005260EE /* RecoveryPhraseBackupFailedView.swift in Sources */, 0D26AED5299E8196005260EE /* RecoveryPhraseBackupFailedView.swift in Sources */,
346731A029AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */,
0D26AED6299E8196005260EE /* UserPreferencesStorageInterface.swift in Sources */, 0D26AED6299E8196005260EE /* UserPreferencesStorageInterface.swift in Sources */,
0D26AED7299E8196005260EE /* DiskSpaceCheckerMocks.swift in Sources */, 0D26AED7299E8196005260EE /* DiskSpaceCheckerMocks.swift in Sources */,
0D26AED8299E8196005260EE /* DropDelegate.swift in Sources */, 0D26AED8299E8196005260EE /* DropDelegate.swift in Sources */,
0D26AED9299E8196005260EE /* LocalAuthenticationLiveKey.swift in Sources */, 0D26AED9299E8196005260EE /* LocalAuthenticationLiveKey.swift in Sources */,
0D26AEDA299E8196005260EE /* ImportSeedEditor.swift in Sources */, 0D26AEDA299E8196005260EE /* ImportSeedEditor.swift in Sources */,
0D26AEDB299E8196005260EE /* ProfileStore.swift in Sources */, 0D26AEDB299E8196005260EE /* ProfileStore.swift in Sources */,
3467319629AE265300974482 /* SupportDataGenerator.swift in Sources */,
0D26AEDC299E8196005260EE /* CheckCircle.swift in Sources */, 0D26AEDC299E8196005260EE /* CheckCircle.swift in Sources */,
0D26AEDD299E8196005260EE /* LogStore.swift in Sources */, 0D26AEDD299E8196005260EE /* LogStore.swift in Sources */,
0D26AEDE299E8196005260EE /* RecoveryPhraseRandomizer.swift in Sources */, 0D26AEDE299E8196005260EE /* RecoveryPhraseRandomizer.swift in Sources */,
3467319D29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */,
0D26AEDF299E8196005260EE /* FileManagerTestKey.swift in Sources */, 0D26AEDF299E8196005260EE /* FileManagerTestKey.swift in Sources */,
0D26AEE0299E8196005260EE /* SecItemLive.swift in Sources */, 0D26AEE0299E8196005260EE /* SecItemLive.swift in Sources */,
0D26AEE1299E8196005260EE /* CircularFrameBadge.swift in Sources */, 0D26AEE1299E8196005260EE /* CircularFrameBadge.swift in Sources */,
@ -2733,6 +2765,7 @@
0D26AF52299E8196005260EE /* LogsHandlerTest.swift in Sources */, 0D26AF52299E8196005260EE /* LogsHandlerTest.swift in Sources */,
0D26AF53299E8196005260EE /* TextFieldFooter.swift in Sources */, 0D26AF53299E8196005260EE /* TextFieldFooter.swift in Sources */,
0D26AF54299E8196005260EE /* CrashReportingInterface.swift in Sources */, 0D26AF54299E8196005260EE /* CrashReportingInterface.swift in Sources */,
346731A329AE3A5100974482 /* UIMailDialog.swift in Sources */,
0D26AF55299E8196005260EE /* ProfileView.swift in Sources */, 0D26AF55299E8196005260EE /* ProfileView.swift in Sources */,
0D26AF56299E8196005260EE /* ScanStore.swift in Sources */, 0D26AF56299E8196005260EE /* ScanStore.swift in Sources */,
0D26AF57299E8196005260EE /* NumberFormatterTestKey.swift in Sources */, 0D26AF57299E8196005260EE /* NumberFormatterTestKey.swift in Sources */,
@ -2781,6 +2814,7 @@
34DA414728E4385800F8CC61 /* TransactionSendingView.swift in Sources */, 34DA414728E4385800F8CC61 /* TransactionSendingView.swift in Sources */,
F96B41E9273B501F0021B49A /* WalletEventsFlowView.swift in Sources */, F96B41E9273B501F0021B49A /* WalletEventsFlowView.swift in Sources */,
9EBDF96E291ECED4000A1A05 /* CaptureDeviceLiveKey.swift in Sources */, 9EBDF96E291ECED4000A1A05 /* CaptureDeviceLiveKey.swift in Sources */,
3467319929AE374300974482 /* SupportDataGeneratorInterface.swift in Sources */,
9EBDF968291ECDA2000A1A05 /* AudioServicesInterface.swift in Sources */, 9EBDF968291ECDA2000A1A05 /* AudioServicesInterface.swift in Sources */,
9EB863BD2923C704003D0F8B /* NotificationCenterTest.swift in Sources */, 9EB863BD2923C704003D0F8B /* NotificationCenterTest.swift in Sources */,
0D26103E298C3FA600CC9DE9 /* CrashReporterLiveKey.swift in Sources */, 0D26103E298C3FA600CC9DE9 /* CrashReporterLiveKey.swift in Sources */,
@ -2829,15 +2863,18 @@
9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */, 9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */,
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */, 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */,
0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */, 0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */,
3467319F29AE375000974482 /* SupportDataGeneratorTestKey.swift in Sources */,
0D63170029919970007D873F /* UserPreferencesStorageInterface.swift in Sources */, 0D63170029919970007D873F /* UserPreferencesStorageInterface.swift in Sources */,
9EBDF94D291D773A000A1A05 /* DiskSpaceCheckerMocks.swift in Sources */, 9EBDF94D291D773A000A1A05 /* DiskSpaceCheckerMocks.swift in Sources */,
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */, 0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */,
9EBDF986291F91EF000A1A05 /* LocalAuthenticationLiveKey.swift in Sources */, 9EBDF986291F91EF000A1A05 /* LocalAuthenticationLiveKey.swift in Sources */,
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */, 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */, F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */,
3467319529AE265300974482 /* SupportDataGenerator.swift in Sources */,
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */, 346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */,
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */, 9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */,
9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */, 9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */,
3467319C29AE374A00974482 /* SupportDataGeneratorLiveKey.swift in Sources */,
9EB863C52923C8AF003D0F8B /* FileManagerTestKey.swift in Sources */, 9EB863C52923C8AF003D0F8B /* FileManagerTestKey.swift in Sources */,
9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */, 9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */,
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
@ -2957,6 +2994,7 @@
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */, 9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */, 2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
0D26103C298C3E4800CC9DE9 /* CrashReportingInterface.swift in Sources */, 0D26103C298C3E4800CC9DE9 /* CrashReportingInterface.swift in Sources */,
346731A229AE3A5100974482 /* UIMailDialog.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */, F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */, F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
9EB863952922D036003D0F8B /* NumberFormatterTestKey.swift in Sources */, 9EB863952922D036003D0F8B /* NumberFormatterTestKey.swift in Sources */,

View File

@ -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)
]
}
}

View File

@ -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
}

View File

@ -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() }
)
}

View File

@ -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: "") }
)
}

View File

@ -37,14 +37,6 @@ struct ProfileView: View {
.frame(height: 50) .frame(height: 50)
.padding(EdgeInsets(top: 30, leading: 30, bottom: 20, trailing: 30)) .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() Spacer()
HStack { HStack {

View File

@ -1,4 +1,5 @@
import ComposableArchitecture import ComposableArchitecture
import MessageUI
import SwiftUI import SwiftUI
typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action> typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action>
@ -10,13 +11,14 @@ struct SettingsReducer: ReducerProtocol {
case backupPhrase case backupPhrase
} }
@BindingState var alert: AlertState<SettingsReducer.Action>?
var destination: Destination? var destination: Destination?
var exportLogsDisabled = false var exportLogsDisabled = false
@BindingState var isCrashReportingOn: Bool
var isSharingLogs = false var isSharingLogs = false
var phraseDisplayState: RecoveryPhraseDisplayReducer.State var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>? var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
var supportData: SupportData?
@BindingState var isCrashReportingOn: Bool
var tempSDKDir: URL { var tempSDKDir: URL {
let tempDir = FileManager.default.temporaryDirectory let tempDir = FileManager.default.temporaryDirectory
@ -42,6 +44,7 @@ struct SettingsReducer: ReducerProtocol {
case backupWalletAccessRequest case backupWalletAccessRequest
case binding(BindingAction<SettingsReducer.State>) case binding(BindingAction<SettingsReducer.State>)
case cancelRescan case cancelRescan
case dismissAlert
case exportLogs case exportLogs
case fullRescan case fullRescan
case logsExported case logsExported
@ -50,8 +53,10 @@ struct SettingsReducer: ReducerProtocol {
case phraseDisplay(RecoveryPhraseDisplayReducer.Action) case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan case quickRescan
case rescanBlockchain case rescanBlockchain
case updateDestination(SettingsReducer.State.Destination?) case sendSupportMail
case sendSupportMailFinished
case testCrashReporter // this will crash the app if live. case testCrashReporter // this will crash the app if live.
case updateDestination(SettingsReducer.State.Destination?)
} }
@Dependency(\.localAuthentication) var localAuthentication @Dependency(\.localAuthentication) var localAuthentication
@ -101,6 +106,10 @@ struct SettingsReducer: ReducerProtocol {
case .cancelRescan, .quickRescan, .fullRescan: case .cancelRescan, .quickRescan, .fullRescan:
state.rescanDialog = nil state.rescanDialog = nil
return .none return .none
case .dismissAlert:
state.alert = nil
return .none
case .exportLogs: case .exportLogs:
state.exportLogsDisabled = true state.exportLogsDisabled = true
@ -148,6 +157,26 @@ struct SettingsReducer: ReducerProtocol {
case .binding: case .binding:
return .none 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 { extension SettingsReducer.State {
static let placeholder = SettingsReducer.State( static let placeholder = SettingsReducer.State(
isCrashReportingOn: true,
phraseDisplayState: RecoveryPhraseDisplayReducer.State( phraseDisplayState: RecoveryPhraseDisplayReducer.State(
phrase: .placeholder phrase: .placeholder
), )
isCrashReportingOn: true
) )
} }

View File

@ -48,6 +48,14 @@ struct SettingsView: View {
) )
.primaryButtonStyle .primaryButtonStyle
.frame(height: 50) .frame(height: 50)
Button(
action: { viewStore.send(.sendSupportMail) },
label: { Text("Send us feedback!") }
)
.primaryButtonStyle
.frame(height: 50)
Spacer() Spacer()
} }
.padding(.horizontal, 30) .padding(.horizontal, 30)
@ -64,6 +72,7 @@ struct SettingsView: View {
} }
) )
.onAppear { viewStore.send(.onAppear) } .onAppear { viewStore.send(.onAppear) }
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
if viewStore.isSharingLogs { if viewStore.isSharingLogs {
UIShareDialogView( UIShareDialogView(
@ -75,6 +84,18 @@ struct SettingsView: View {
// so frame is set to 0 to not break SwiftUIs layout // so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0) .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)
}
} }
} }
} }

View File

@ -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
}

View File

@ -46,8 +46,8 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil), isCrashReportingOn: false,
isCrashReportingOn: false phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) { dependencies in ) { dependencies in
@ -103,6 +103,7 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil, destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -112,8 +113,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
isCrashReportingOn: false
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -127,6 +127,7 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil, destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -136,8 +137,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
isCrashReportingOn: false
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -151,6 +151,7 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil, destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -160,8 +161,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
isCrashReportingOn: false
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -175,6 +175,7 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil, destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -184,8 +185,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
isCrashReportingOn: false
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -206,6 +206,7 @@ class SettingsTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil, destination: nil,
isCrashReportingOn: false,
isSharingLogs: true, isSharingLogs: true,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
@ -216,8 +217,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
isCrashReportingOn: false
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )