- flow is hidden by default - when turned on, only new users (nuke wallet and start over) continues to the flow - unit tests fixed and updated - new unit tests - phrase display screen simplified [#556] Hide post-seed backup flow and rework screenshot tests (#591) - never show the phrase for users who had it disabled at the time of wallet creation
This commit is contained in:
parent
2a560dea8d
commit
b61fa213b6
|
@ -446,6 +446,7 @@
|
||||||
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; };
|
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; };
|
||||||
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; };
|
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; };
|
||||||
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; };
|
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; };
|
||||||
|
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */; };
|
||||||
9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; };
|
9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; };
|
||||||
9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */; };
|
9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */; };
|
||||||
9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; };
|
9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; };
|
||||||
|
@ -768,6 +769,7 @@
|
||||||
9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWallet.swift; sourceTree = "<group>"; };
|
9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWallet.swift; sourceTree = "<group>"; };
|
||||||
9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanUIView.swift; sourceTree = "<group>"; };
|
9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanUIView.swift; sourceTree = "<group>"; };
|
||||||
9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = "<group>"; };
|
9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = "<group>"; };
|
||||||
|
9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationFlowFeatureFlagTests.swift; sourceTree = "<group>"; };
|
||||||
9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = "<group>"; };
|
9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = "<group>"; };
|
||||||
9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = "<group>"; };
|
9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = "<group>"; };
|
||||||
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
|
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1091,7 +1093,6 @@
|
||||||
children = (
|
children = (
|
||||||
9EF8135F27F043CC0075AF48 /* AppDelegate.swift */,
|
9EF8135F27F043CC0075AF48 /* AppDelegate.swift */,
|
||||||
0D6D628A276A528D002FB4CC /* DropDelegate.swift */,
|
0D6D628A276A528D002FB4CC /* DropDelegate.swift */,
|
||||||
34F682E429A75EB60022C079 /* WalletConfig.swift */,
|
|
||||||
9EF8139B27F47AED0075AF48 /* InitializationState.swift */,
|
9EF8139B27F47AED0075AF48 /* InitializationState.swift */,
|
||||||
9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */,
|
9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */,
|
||||||
9E612C7C2991476F00D09B09 /* SensitiveData.swift */,
|
9E612C7C2991476F00D09B09 /* SensitiveData.swift */,
|
||||||
|
@ -1099,6 +1100,7 @@
|
||||||
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */,
|
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */,
|
||||||
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */,
|
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */,
|
||||||
9E7FE0DC282D298900C374E8 /* ValidationWord.swift */,
|
9E7FE0DC282D298900C374E8 /* ValidationWord.swift */,
|
||||||
|
34F682E429A75EB60022C079 /* WalletConfig.swift */,
|
||||||
9EAB46772860A1D2002904A0 /* WalletEvent.swift */,
|
9EAB46772860A1D2002904A0 /* WalletEvent.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
|
@ -1145,6 +1147,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */,
|
0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */,
|
||||||
|
9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */,
|
||||||
);
|
);
|
||||||
path = RecoveryPhraseValidationTests;
|
path = RecoveryPhraseValidationTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3052,6 +3055,7 @@
|
||||||
9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */,
|
9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */,
|
||||||
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */,
|
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */,
|
||||||
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
|
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
|
||||||
|
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */,
|
||||||
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
||||||
9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */,
|
9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */,
|
||||||
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */,
|
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */,
|
||||||
|
|
|
@ -15,5 +15,5 @@ extension DependencyValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecoveryPhraseRandomizerClient {
|
struct RecoveryPhraseRandomizerClient {
|
||||||
let random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State
|
var random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,10 +116,10 @@ struct WalletStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markUserPassedPhraseBackupTest() throws {
|
func markUserPassedPhraseBackupTest(_ flag: Bool = true) throws {
|
||||||
do {
|
do {
|
||||||
var wallet = try exportWallet()
|
var wallet = try exportWallet()
|
||||||
wallet.hasUserPassedPhraseBackupTest = true
|
wallet.hasUserPassedPhraseBackupTest = flag
|
||||||
|
|
||||||
guard let data = try encode(object: wallet) else {
|
guard let data = try encode(object: wallet) else {
|
||||||
throw KeychainError.encoding
|
throw KeychainError.encoding
|
||||||
|
|
|
@ -30,6 +30,7 @@ struct WalletStorageClient {
|
||||||
/// - bip39: Mnemonic/Seed phrase from `MnemonicSwift`
|
/// - bip39: Mnemonic/Seed phrase from `MnemonicSwift`
|
||||||
/// - birthday: BlockHeight from SDK
|
/// - birthday: BlockHeight from SDK
|
||||||
/// - language: Mnemonic's language
|
/// - language: Mnemonic's language
|
||||||
|
/// - hasUserPassedPhraseBackupTest: If user passed the puzzle phrase backup
|
||||||
/// - Throws:
|
/// - Throws:
|
||||||
/// - `WalletStorageError.unsupportedLanguage`: when mnemonic's language is anything other than English
|
/// - `WalletStorageError.unsupportedLanguage`: when mnemonic's language is anything other than English
|
||||||
/// - `WalletStorageError.alreadyImported` when valid wallet is already in the storage
|
/// - `WalletStorageError.alreadyImported` when valid wallet is already in the storage
|
||||||
|
@ -66,7 +67,7 @@ struct WalletStorageClient {
|
||||||
/// - Throws:
|
/// - Throws:
|
||||||
/// - `WalletStorage.KeychainError.encoding`: when encoding the wallet's data failed.
|
/// - `WalletStorage.KeychainError.encoding`: when encoding the wallet's data failed.
|
||||||
/// - `WalletStorageError.storageError` when some unrecognized error occurred.
|
/// - `WalletStorageError.storageError` when some unrecognized error occurred.
|
||||||
let markUserPassedPhraseBackupTest: () throws -> Void
|
let markUserPassedPhraseBackupTest: (Bool) throws -> Void
|
||||||
|
|
||||||
/// Use carefully: deletes the stored wallet.
|
/// Use carefully: deletes the stored wallet.
|
||||||
/// There's no fate but what we make for ourselves - Sarah Connor.
|
/// There's no fate but what we make for ourselves - Sarah Connor.
|
||||||
|
|
|
@ -32,8 +32,8 @@ extension WalletStorageClient: DependencyKey {
|
||||||
updateBirthday: { birthday in
|
updateBirthday: { birthday in
|
||||||
try walletStorage.updateBirthday(birthday)
|
try walletStorage.updateBirthday(birthday)
|
||||||
},
|
},
|
||||||
markUserPassedPhraseBackupTest: {
|
markUserPassedPhraseBackupTest: { flag in
|
||||||
try walletStorage.markUserPassedPhraseBackupTest()
|
try walletStorage.markUserPassedPhraseBackupTest(flag)
|
||||||
},
|
},
|
||||||
nukeWallet: {
|
nukeWallet: {
|
||||||
walletStorage.nukeWallet()
|
walletStorage.nukeWallet()
|
||||||
|
|
|
@ -25,7 +25,7 @@ extension WalletStorageClient {
|
||||||
exportWallet: { .placeholder },
|
exportWallet: { .placeholder },
|
||||||
areKeysPresent: { false },
|
areKeysPresent: { false },
|
||||||
updateBirthday: { _ in },
|
updateBirthday: { _ in },
|
||||||
markUserPassedPhraseBackupTest: { },
|
markUserPassedPhraseBackupTest: { _ in },
|
||||||
nukeWallet: { }
|
nukeWallet: { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ struct ImportWalletReducer: ReducerProtocol {
|
||||||
try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false)
|
try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false)
|
||||||
|
|
||||||
// update the backup phrase validation flag
|
// update the backup phrase validation flag
|
||||||
try walletStorage.markUserPassedPhraseBackupTest()
|
try walletStorage.markUserPassedPhraseBackupTest(true)
|
||||||
|
|
||||||
// notify user
|
// notify user
|
||||||
// TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221)
|
// TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||||
|
|
|
@ -33,7 +33,6 @@ struct RecoveryPhraseDisplayReducer: ReducerProtocol {
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .finishedPressed:
|
case .finishedPressed:
|
||||||
// TODO: [#47] remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/47
|
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case let .phraseResponse(phrase):
|
case let .phraseResponse(phrase):
|
||||||
|
|
|
@ -13,57 +13,57 @@ struct RecoveryPhraseDisplayView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
WithViewStore(self.store) { viewStore in
|
WithViewStore(self.store) { viewStore in
|
||||||
ScrollView {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
if let groups = viewStore.phrase?.toGroups(groupSizeOverride: 2) {
|
||||||
if let groups = viewStore.phrase?.toGroups() {
|
VStack(spacing: 20) {
|
||||||
VStack(spacing: 20) {
|
Text("recoveryPhraseDisplay.title")
|
||||||
Text("recoveryPhraseDisplay.title")
|
.titleText()
|
||||||
.titleText()
|
.multilineTextAlignment(.center)
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
VStack(alignment: .center, spacing: 4) {
|
|
||||||
Text("recoveryPhraseDisplay.description")
|
|
||||||
.bodyText()
|
|
||||||
|
|
||||||
Text("recoveryPhraseDisplay.backItUp")
|
|
||||||
.bodyText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 0)
|
|
||||||
.padding(.bottom, 20)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 35) {
|
VStack(alignment: .center, spacing: 4) {
|
||||||
ForEach(groups, id: \.startIndex) { group in
|
Text("recoveryPhraseDisplay.description")
|
||||||
VStack {
|
.bodyText()
|
||||||
WordChipGrid(words: group.words, startingAt: group.startIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 5)
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
Button(
|
|
||||||
action: { viewStore.send(.finishedPressed) },
|
|
||||||
label: { Text("recoveryPhraseDisplay.button.finished") }
|
|
||||||
)
|
|
||||||
.activeButtonStyle
|
|
||||||
.frame(height: 60)
|
|
||||||
|
|
||||||
Button(
|
|
||||||
action: {
|
|
||||||
viewStore.send(.copyToBufferPressed)
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
Text("recoveryPhraseDisplay.button.copyToBuffer")
|
|
||||||
.bodyText()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(height: 60)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
} else {
|
|
||||||
Text("recoveryPhraseDisplay.noWords")
|
|
||||||
}
|
}
|
||||||
|
.padding(.top, 0)
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
ForEach(groups, id: \.startIndex) { group in
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("\(group.startIndex). \(group.words[0].data)")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.leading, 20)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("\(group.startIndex + 1). \(group.words[1].data)")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.trailing, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Button(
|
||||||
|
action: { viewStore.send(.finishedPressed) },
|
||||||
|
label: { Text("recoveryPhraseDisplay.button.wroteItDown") }
|
||||||
|
)
|
||||||
|
.activeButtonStyle
|
||||||
|
.frame(height: 60)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else {
|
||||||
|
Text("recoveryPhraseDisplay.noWords")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
|
@ -62,12 +62,15 @@ extension RootReducer {
|
||||||
|
|
||||||
case .phraseDisplay(.finishedPressed):
|
case .phraseDisplay(.finishedPressed):
|
||||||
// user is still supposed to do the backup phrase validation test
|
// user is still supposed to do the backup phrase validation test
|
||||||
if state.destinationState.previousDestination == .welcome
|
if (state.destinationState.previousDestination == .welcome
|
||||||
|| state.destinationState.previousDestination == .onboarding {
|
|| state.destinationState.previousDestination == .onboarding
|
||||||
|
|| state.destinationState.previousDestination == .startup)
|
||||||
|
&& state.walletConfig.isEnabled(.testBackupPhraseFlow) {
|
||||||
state.destinationState.destination = .phraseValidation
|
state.destinationState.destination = .phraseValidation
|
||||||
}
|
}
|
||||||
// user wanted to see the backup phrase once again (at validation finished screen)
|
// user wanted to see the backup phrase once again (at validation finished screen)
|
||||||
if state.destinationState.previousDestination == .phraseValidation {
|
if state.destinationState.previousDestination == .phraseValidation
|
||||||
|
|| !state.walletConfig.isEnabled(.testBackupPhraseFlow) {
|
||||||
state.destinationState.destination = .home
|
state.destinationState.destination = .home
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,8 +149,8 @@ extension RootReducer {
|
||||||
}
|
}
|
||||||
|
|
||||||
var landingDestination = RootReducer.DestinationState.Destination.home
|
var landingDestination = RootReducer.DestinationState.Destination.home
|
||||||
|
|
||||||
if !storedWallet.hasUserPassedPhraseBackupTest {
|
if !storedWallet.hasUserPassedPhraseBackupTest && state.walletConfig.isEnabled(.testBackupPhraseFlow) {
|
||||||
do {
|
do {
|
||||||
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value())
|
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value())
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ extension RootReducer {
|
||||||
let birthday = zcashSDKEnvironment.latestCheckpoint
|
let birthday = zcashSDKEnvironment.latestCheckpoint
|
||||||
|
|
||||||
// store the wallet to the keychain
|
// store the wallet to the keychain
|
||||||
try walletStorage.importWallet(newRandomPhrase, birthday, .english, false)
|
try walletStorage.importWallet(newRandomPhrase, birthday, .english, !state.walletConfig.isEnabled(.testBackupPhraseFlow))
|
||||||
|
|
||||||
// start the backup phrase validation test
|
// start the backup phrase validation test
|
||||||
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
|
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
|
||||||
|
@ -198,7 +198,7 @@ extension RootReducer {
|
||||||
|
|
||||||
case .phraseValidation(.succeed):
|
case .phraseValidation(.succeed):
|
||||||
do {
|
do {
|
||||||
try walletStorage.markUserPassedPhraseBackupTest()
|
try walletStorage.markUserPassedPhraseBackupTest(true)
|
||||||
} catch {
|
} catch {
|
||||||
// TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
// TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,11 @@ struct RecoveryPhrase: Equatable, Redactable {
|
||||||
|
|
||||||
private let groupSize = 6
|
private let groupSize = 6
|
||||||
|
|
||||||
func toGroups() -> [Group] {
|
func toGroups(groupSizeOverride: Int? = nil) -> [Group] {
|
||||||
let chunks = words.count / groupSize
|
let internalGroupSize = groupSizeOverride ?? groupSize
|
||||||
return zip(0 ..< chunks, words.chunked(into: groupSize)).map {
|
let chunks = words.count / internalGroupSize
|
||||||
Group(startIndex: $0 * groupSize + 1, words: $1)
|
return zip(0 ..< chunks, words.chunked(into: internalGroupSize)).map {
|
||||||
|
Group(startIndex: $0 * internalGroupSize + 1, words: $1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,13 @@ enum FeatureFlag: String, CaseIterable, Codable {
|
||||||
case testFlag1
|
case testFlag1
|
||||||
case testFlag2
|
case testFlag2
|
||||||
case onboardingFlow
|
case onboardingFlow
|
||||||
|
case testBackupPhraseFlow
|
||||||
|
|
||||||
var enabledByDefault: Bool {
|
var enabledByDefault: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .testFlag1, .testFlag2: return false
|
case .testFlag1, .testFlag2: return false
|
||||||
case .onboardingFlow: return false
|
case .onboardingFlow: return false
|
||||||
|
case .testBackupPhraseFlow: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,8 @@
|
||||||
|
|
||||||
// MARK: - Secret Recovery Phrase Display
|
// MARK: - Secret Recovery Phrase Display
|
||||||
"recoveryPhraseDisplay.title" = "Your Secret Recovery Phrase";
|
"recoveryPhraseDisplay.title" = "Your Secret Recovery Phrase";
|
||||||
"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them.";
|
"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them. Back them up now!";
|
||||||
"recoveryPhraseDisplay.backItUp" = "Back them up now! There will be a test.";
|
"recoveryPhraseDisplay.button.wroteItDown" = "I wrote it down!";
|
||||||
"recoveryPhraseDisplay.button.finished" = "Finished!";
|
|
||||||
"recoveryPhraseDisplay.button.copyToBuffer" = "Copy To Buffer";
|
"recoveryPhraseDisplay.button.copyToBuffer" = "Copy To Buffer";
|
||||||
"recoveryPhraseDisplay.noWords" = "Oops no words";
|
"recoveryPhraseDisplay.noWords" = "Oops no words";
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ extension Text {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.foregroundColor(Asset.Colors.Text.heading.color)
|
.foregroundColor(Asset.Colors.Text.heading.color)
|
||||||
.font(.custom(FontFamily.Rubik.medium.name, size: 33, relativeTo: .callout))
|
.font(.custom(FontFamily.Rubik.medium.name, size: 24, relativeTo: .callout))
|
||||||
.shadow(color: Asset.Colors.Text.captionTextShadow.color, radius: 1, x: 0, y: 1)
|
.shadow(color: Asset.Colors.Text.captionTextShadow.color, radius: 1, x: 0, y: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
//
|
||||||
|
// RecoveryPhraseValidationFlowFeatureFlagTests.swift
|
||||||
|
// secantTests
|
||||||
|
//
|
||||||
|
// Created by Lukáš Korba on 01.03.2023.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import secant_testnet
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// swiftlint:disable:next type_name
|
||||||
|
class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase {
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
|
UserDefaultsWalletConfigStorage().clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecoveryPhraseValidationFlowOffByDefault() throws {
|
||||||
|
XCTAssertFalse(WalletConfig.default.isEnabled(.testBackupPhraseFlow))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecoveryPhraseValidationFlow_SkipPuzzleUserConfirmedBackup() {
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: .placeholder,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
store.send(.phraseDisplay(.finishedPressed)) { state in
|
||||||
|
state.destinationState.internalDestination = .home
|
||||||
|
state.destinationState.previousDestination = .welcome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecoveryPhraseValidationFlow_StartPuzzle() {
|
||||||
|
var rootState = RootReducer.State.placeholder
|
||||||
|
|
||||||
|
var defaultRawFlags = WalletConfig.default.flags
|
||||||
|
defaultRawFlags[.testBackupPhraseFlow] = true
|
||||||
|
rootState.walletConfig = WalletConfig(flags: defaultRawFlags)
|
||||||
|
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: rootState,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
store.send(.phraseDisplay(.finishedPressed))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecoveryPhraseValidationFlow_SkipPuzzleStartOfTheApp() {
|
||||||
|
var rootState = RootReducer.State.placeholder
|
||||||
|
rootState.storedWallet = .placeholder
|
||||||
|
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: rootState,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
store.dependencies.mainQueue = .immediate
|
||||||
|
|
||||||
|
store.send(.initialization(.checkBackupPhraseValidation)) { state in
|
||||||
|
state.appInitializationState = .initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
store.receive(.destination(.updateDestination(.home))) { state in
|
||||||
|
state.destinationState.internalDestination = .home
|
||||||
|
state.destinationState.previousDestination = .welcome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecoveryPhraseValidationFlow_StartPuzzleStartOfTheApp() {
|
||||||
|
var rootState = RootReducer.State.placeholder
|
||||||
|
rootState.storedWallet = .placeholder
|
||||||
|
|
||||||
|
var defaultRawFlags = WalletConfig.default.flags
|
||||||
|
defaultRawFlags[.testBackupPhraseFlow] = true
|
||||||
|
rootState.walletConfig = WalletConfig(flags: defaultRawFlags)
|
||||||
|
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: rootState,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
let mnemonic =
|
||||||
|
"""
|
||||||
|
still champion voice habit trend flight \
|
||||||
|
survey between bitter process artefact blind \
|
||||||
|
carbon truly provide dizzy crush flush \
|
||||||
|
breeze blouse charge solid fish spread
|
||||||
|
"""
|
||||||
|
|
||||||
|
let randomRecoveryPhrase = RecoveryPhraseValidationFlowReducer.State.placeholder
|
||||||
|
|
||||||
|
store.dependencies.mainQueue = .immediate
|
||||||
|
store.dependencies.mnemonic = .noOp
|
||||||
|
store.dependencies.mnemonic.asWords = { _ in mnemonic.components(separatedBy: " ") }
|
||||||
|
store.dependencies.randomRecoveryPhrase.random = { _ in randomRecoveryPhrase }
|
||||||
|
|
||||||
|
store.send(.initialization(.checkBackupPhraseValidation)) { state in
|
||||||
|
state.appInitializationState = .initialized
|
||||||
|
state.phraseDisplayState.phrase = RecoveryPhrase(words: mnemonic.components(separatedBy: " ").map { $0.redacted })
|
||||||
|
}
|
||||||
|
|
||||||
|
store.receive(.destination(.updateDestination(.phraseDisplay))) { state in
|
||||||
|
state.destinationState.internalDestination = .phraseDisplay
|
||||||
|
state.destinationState.previousDestination = .welcome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor func testDidFinishLaunching_to_InitializedWalletPhraseBackupPuzzleOff() async throws {
|
||||||
|
// setup the store and environment to be fully mocked
|
||||||
|
let recoveryPhrase = RecoveryPhrase(words: try MnemonicClient.mock.randomMnemonicWords().map { $0.redacted })
|
||||||
|
|
||||||
|
let phraseValidationState = RecoveryPhraseValidationFlowReducer.State(
|
||||||
|
phrase: recoveryPhrase,
|
||||||
|
missingIndices: [2, 0, 3, 5],
|
||||||
|
missingWordChips: [
|
||||||
|
.unassigned(word: "voice".redacted),
|
||||||
|
.empty,
|
||||||
|
.unassigned(word: "survey".redacted),
|
||||||
|
.unassigned(word: "spread".redacted)
|
||||||
|
],
|
||||||
|
validationWords: [
|
||||||
|
.init(groupIndex: 2, word: "dizzy".redacted)
|
||||||
|
],
|
||||||
|
destination: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let recoveryPhraseRandomizer = RecoveryPhraseRandomizerClient(
|
||||||
|
random: { _ in
|
||||||
|
let missingIndices = [2, 0, 3, 5]
|
||||||
|
let missingWordChipKind = [
|
||||||
|
PhraseChip.Kind.unassigned(
|
||||||
|
word: "voice".redacted,
|
||||||
|
color: Asset.Colors.Buttons.activeButton.color
|
||||||
|
),
|
||||||
|
PhraseChip.Kind.empty,
|
||||||
|
PhraseChip.Kind.unassigned(
|
||||||
|
word: "survey".redacted,
|
||||||
|
color: Asset.Colors.Buttons.activeButton.color
|
||||||
|
),
|
||||||
|
PhraseChip.Kind.unassigned(
|
||||||
|
word: "spread".redacted,
|
||||||
|
color: Asset.Colors.Buttons.activeButton.color
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return RecoveryPhraseValidationFlowReducer.State(
|
||||||
|
phrase: recoveryPhrase,
|
||||||
|
missingIndices: missingIndices,
|
||||||
|
missingWordChips: missingWordChipKind,
|
||||||
|
validationWords: [
|
||||||
|
ValidationWord(
|
||||||
|
groupIndex: 2,
|
||||||
|
word: "dizzy".redacted
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let appState = RootReducer.State(
|
||||||
|
destinationState: .placeholder,
|
||||||
|
homeState: .placeholder,
|
||||||
|
onboardingState: .init(
|
||||||
|
walletConfig: .default,
|
||||||
|
importWalletState: .placeholder
|
||||||
|
),
|
||||||
|
phraseValidationState: phraseValidationState,
|
||||||
|
phraseDisplayState: RecoveryPhraseDisplayReducer.State(
|
||||||
|
phrase: recoveryPhrase
|
||||||
|
),
|
||||||
|
sandboxState: .placeholder,
|
||||||
|
walletConfig: .default,
|
||||||
|
welcomeState: .placeholder
|
||||||
|
)
|
||||||
|
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: appState,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
let testQueue = DispatchQueue.test
|
||||||
|
|
||||||
|
store.dependencies.databaseFiles = .noOp
|
||||||
|
store.dependencies.databaseFiles.areDbFilesPresentFor = { _ in true }
|
||||||
|
store.dependencies.derivationTool = .liveValue
|
||||||
|
store.dependencies.mainQueue = .immediate// testQueue.eraseToAnyScheduler()
|
||||||
|
store.dependencies.mnemonic = .mock
|
||||||
|
store.dependencies.randomRecoveryPhrase = recoveryPhraseRandomizer
|
||||||
|
store.dependencies.walletStorage.exportWallet = { .placeholder }
|
||||||
|
store.dependencies.walletStorage.areKeysPresent = { true }
|
||||||
|
store.dependencies.walletConfigProvider = .noOp
|
||||||
|
|
||||||
|
// Root of the test, the app finished the launch process and triggers the checks and initializations.
|
||||||
|
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
|
||||||
|
|
||||||
|
await testQueue.advance(by: 0.02)
|
||||||
|
|
||||||
|
await store.receive(.initialization(.checkWalletConfig))
|
||||||
|
|
||||||
|
await store.receive(.walletConfigLoaded(WalletConfig.default))
|
||||||
|
|
||||||
|
await store.receive(.initialization(.initialSetups))
|
||||||
|
|
||||||
|
await testQueue.advance(by: 0.02)
|
||||||
|
|
||||||
|
await store.receive(.initialization(.configureCrashReporter))
|
||||||
|
|
||||||
|
await store.receive(.initialization(.checkWalletInitialization))
|
||||||
|
|
||||||
|
await store.receive(.initialization(.respondToWalletInitializationState(.initialized)))
|
||||||
|
|
||||||
|
await testQueue.advance(by: 3.00)
|
||||||
|
|
||||||
|
await store.receive(.initialization(.initializeSDK)) { state in
|
||||||
|
state.storedWallet = .placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.receive(.initialization(.checkBackupPhraseValidation)) { state in
|
||||||
|
state.appInitializationState = .initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.receive(.destination(.updateDestination(.home))) { state in
|
||||||
|
state.destinationState.previousDestination = .welcome
|
||||||
|
state.destinationState.internalDestination = .home
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,10 @@ class AppInitializationTests: XCTestCase {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultRawFlags = WalletConfig.default.flags
|
||||||
|
defaultRawFlags[.testBackupPhraseFlow] = true
|
||||||
|
let walletConfig = WalletConfig(flags: defaultRawFlags)
|
||||||
|
|
||||||
let appState = RootReducer.State(
|
let appState = RootReducer.State(
|
||||||
destinationState: .placeholder,
|
destinationState: .placeholder,
|
||||||
homeState: .placeholder,
|
homeState: .placeholder,
|
||||||
|
@ -75,7 +79,7 @@ class AppInitializationTests: XCTestCase {
|
||||||
phrase: recoveryPhrase
|
phrase: recoveryPhrase
|
||||||
),
|
),
|
||||||
sandboxState: .placeholder,
|
sandboxState: .placeholder,
|
||||||
walletConfig: .default,
|
walletConfig: walletConfig,
|
||||||
welcomeState: .placeholder
|
welcomeState: .placeholder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class SettingsTests: XCTestCase {
|
||||||
updateBirthday: { _ in
|
updateBirthday: { _ in
|
||||||
throw WalletStorage.KeychainError.encoding
|
throw WalletStorage.KeychainError.encoding
|
||||||
},
|
},
|
||||||
markUserPassedPhraseBackupTest: {
|
markUserPassedPhraseBackupTest: { _ in
|
||||||
throw WalletStorage.KeychainError.encoding
|
throw WalletStorage.KeychainError.encoding
|
||||||
},
|
},
|
||||||
nukeWallet: { }
|
nukeWallet: { }
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Combine
|
import Combine
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import secant_testnet
|
@testable import secant_testnet
|
||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
class WalletConfigProviderTests: XCTestCase {
|
class WalletConfigProviderTests: XCTestCase {
|
||||||
var cancellables: [AnyCancellable] = []
|
var cancellables: [AnyCancellable] = []
|
||||||
|
@ -114,6 +115,24 @@ class WalletConfigProviderTests: XCTestCase {
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPropagationOfFlagUpdate() throws {
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: .placeholder,
|
||||||
|
reducer: RootReducer()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Change any of the flags from the default value
|
||||||
|
var defaultRawFlags = WalletConfig.default.flags
|
||||||
|
defaultRawFlags[.onboardingFlow] = true
|
||||||
|
let flags = WalletConfig(flags: defaultRawFlags)
|
||||||
|
|
||||||
|
// The new flag's value has to be propagated to all `walletConfig` instances
|
||||||
|
store.send(.debug(.walletConfigLoaded(flags))) { state in
|
||||||
|
state.walletConfig = flags
|
||||||
|
state.onboardingState.walletConfig = flags
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WalletConfigSourceProviderMock: WalletConfigSourceProvider {
|
struct WalletConfigSourceProviderMock: WalletConfigSourceProvider {
|
||||||
|
|
Loading…
Reference in New Issue