- changelog update - the keys missing error state has been tweaked to try 3 retry attempts because of unresponsiveness keychain API - in case of true missing keys, the user is no longer locked on a splash screen but rather let land to the Account tab so the rest of the Zashi can be used - unit tests fixed + implemented new ones for the 3-attempt retry logic
This commit is contained in:
parent
b6248fdd3c
commit
79ab841f75
|
@ -9,6 +9,9 @@ directly impact users rather than highlighting other crucial architectural updat
|
|||
### Added
|
||||
- Share QR code of addresses via system share dialog.
|
||||
|
||||
### Fixed
|
||||
- `Keys Missing` error dialog was sometimes triggered as a false positive due to system overload and keychain API unresponsivity in expected time. Retry logic was implemented to pass this state. Also the app always lands users to the Account tab instead of lock them on a splash screen with no options to solve this state.
|
||||
|
||||
## 0.2.0 build 12 (2024-01-20)
|
||||
|
||||
### Added
|
||||
|
|
|
@ -82,6 +82,8 @@ public struct RecoveryPhraseDisplayView: View {
|
|||
.padding(.bottom, 50)
|
||||
} else {
|
||||
Text(L10n.RecoveryPhraseDisplay.noWords)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 60)
|
||||
|
|
|
@ -25,6 +25,7 @@ extension RootReducer {
|
|||
case initialSetups
|
||||
case initializationFailed(ZcashError)
|
||||
case initializationSuccessfullyDone(UnifiedAddress?)
|
||||
case retryKeychainRead(InitializationState)
|
||||
case nukeWallet
|
||||
case nukeWalletRequest
|
||||
case respondToWalletInitializationState(InitializationState)
|
||||
|
@ -183,12 +184,10 @@ extension RootReducer {
|
|||
switch walletState {
|
||||
case .failed:
|
||||
state.appInitializationState = .failed
|
||||
state.alert = AlertState.walletStateFailed(walletState)
|
||||
return .none
|
||||
return .send(.initialization(.retryKeychainRead(walletState)))
|
||||
case .keysMissing:
|
||||
state.appInitializationState = .keysMissing
|
||||
state.alert = AlertState.walletStateFailed(walletState)
|
||||
return .none
|
||||
return .send(.initialization(.retryKeychainRead(walletState)))
|
||||
case .initialized, .filesMissing:
|
||||
if walletState == .filesMissing {
|
||||
state.appInitializationState = .filesMissing
|
||||
|
@ -206,6 +205,18 @@ extension RootReducer {
|
|||
.cancellable(id: CancelId.timer, cancelInFlight: true)
|
||||
}
|
||||
|
||||
case .initialization(.retryKeychainRead(let walletState)):
|
||||
if state.keychainReadRetries < state.maxKeychainReadRetries {
|
||||
state.keychainReadRetries += 1
|
||||
return .run { [retries = state.keychainReadRetries] send in
|
||||
try await mainQueue.sleep(for: .seconds(0.1 * Double(retries)))
|
||||
await send(.initialization(.checkWalletInitialization))
|
||||
}
|
||||
} else {
|
||||
state.alert = AlertState.walletStateFailed(walletState)
|
||||
return .send(.destination(.updateDestination(RootReducer.DestinationState.Destination.tabs)))
|
||||
}
|
||||
|
||||
/// Stored wallet is present, database files may or may not be present, trying to initialize app state variables and environments.
|
||||
/// When initialization succeeds user is taken to the home screen.
|
||||
case .initialization(.initializeSDK(let walletMode)):
|
||||
|
|
|
@ -40,6 +40,8 @@ public struct RootReducer: Reducer {
|
|||
public var destinationState: DestinationState
|
||||
public var exportLogsState: ExportLogsReducer.State
|
||||
public var isRestoringWallet = false
|
||||
public var keychainReadRetries = 0
|
||||
public var maxKeychainReadRetries = 3
|
||||
public var onboardingState: OnboardingFlowReducer.State
|
||||
public var phraseDisplayState: RecoveryPhraseDisplayReducer.State
|
||||
public var sandboxState: SandboxReducer.State
|
||||
|
@ -55,6 +57,8 @@ public struct RootReducer: Reducer {
|
|||
destinationState: DestinationState,
|
||||
exportLogsState: ExportLogsReducer.State,
|
||||
isRestoringWallet: Bool = false,
|
||||
keychainReadRetries: Int = 0,
|
||||
maxKeychainReadRetries: Int = 3,
|
||||
onboardingState: OnboardingFlowReducer.State,
|
||||
phraseDisplayState: RecoveryPhraseDisplayReducer.State,
|
||||
sandboxState: SandboxReducer.State,
|
||||
|
@ -68,6 +72,8 @@ public struct RootReducer: Reducer {
|
|||
self.destinationState = destinationState
|
||||
self.exportLogsState = exportLogsState
|
||||
self.isRestoringWallet = isRestoringWallet
|
||||
self.keychainReadRetries = keychainReadRetries
|
||||
self.maxKeychainReadRetries = maxKeychainReadRetries
|
||||
self.onboardingState = onboardingState
|
||||
self.phraseDisplayState = phraseDisplayState
|
||||
self.sandboxState = sandboxState
|
||||
|
|
|
@ -306,8 +306,8 @@ public enum L10n {
|
|||
}
|
||||
/// The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!
|
||||
public static let description = L10n.tr("Localizable", "recoveryPhraseDisplay.description", fallback: "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!")
|
||||
/// Oops no words
|
||||
public static let noWords = L10n.tr("Localizable", "recoveryPhraseDisplay.noWords", fallback: "Oops no words")
|
||||
/// The keys are missing. No backup phrase is stored in the keychain.
|
||||
public static let noWords = L10n.tr("Localizable", "recoveryPhraseDisplay.noWords", fallback: "The keys are missing. No backup phrase is stored in the keychain.")
|
||||
/// Your Secret
|
||||
public static let titlePart1 = L10n.tr("Localizable", "recoveryPhraseDisplay.titlePart1", fallback: "Your Secret")
|
||||
/// Recovery Phrase
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"recoveryPhraseDisplay.description" = "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!";
|
||||
"recoveryPhraseDisplay.button.wroteItDown" = "I got it!";
|
||||
"recoveryPhraseDisplay.button.copyToBuffer" = "Copy To Buffer";
|
||||
"recoveryPhraseDisplay.noWords" = "Oops no words";
|
||||
"recoveryPhraseDisplay.noWords" = "The keys are missing. No backup phrase is stored in the keychain.";
|
||||
"recoveryPhraseDisplay.birthdayHeight" = "Wallet birthday height: %@";
|
||||
"recoveryPhraseDisplay.alert.failed.title" = "Failed to load stored wallet";
|
||||
"recoveryPhraseDisplay.alert.failed.message" = "Attempt to load the stored wallet from the keychain failed. Error: %@ (code: %@)";
|
||||
|
|
|
@ -174,8 +174,11 @@ class AppInitializationTests: XCTestCase {
|
|||
|
||||
/// Integration test validating the side effects work together properly when no wallet is stored but database files are present.
|
||||
@MainActor func testDidFinishLaunching_to_KeysMissing() async throws {
|
||||
var initialState = RootReducer.State.initial
|
||||
initialState.keychainReadRetries = 3
|
||||
|
||||
let store = TestStore(
|
||||
initialState: .initial
|
||||
initialState: initialState
|
||||
) {
|
||||
RootReducer(tokenName: "ZEC", zcashNetwork: ZcashNetworkBuilder.network(for: .testnet))
|
||||
}
|
||||
|
@ -199,9 +202,79 @@ class AppInitializationTests: XCTestCase {
|
|||
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
|
||||
state.appInitializationState = .keysMissing
|
||||
}
|
||||
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.alert = AlertState.walletStateFailed(.keysMissing)
|
||||
}
|
||||
|
||||
await store.receive(.destination(.updateDestination(.tabs))) { state in
|
||||
state.destinationState.internalDestination = .tabs
|
||||
state.destinationState.previousDestination = .welcome
|
||||
}
|
||||
|
||||
await store.finish()
|
||||
}
|
||||
|
||||
@MainActor func testDidFinishLaunching_to_KeysMissing_3attempts() async throws {
|
||||
let store = TestStore(
|
||||
initialState: .initial
|
||||
) {
|
||||
RootReducer(tokenName: "ZEC", zcashNetwork: ZcashNetworkBuilder.network(for: .testnet))
|
||||
}
|
||||
|
||||
store.dependencies.databaseFiles = .noOp
|
||||
store.dependencies.databaseFiles.areDbFilesPresentFor = { _ in true }
|
||||
store.dependencies.walletStorage = .noOp
|
||||
store.dependencies.mainQueue = .immediate
|
||||
store.dependencies.walletConfigProvider = .noOp
|
||||
store.dependencies.crashReporter = .noOp
|
||||
store.dependencies.restoreWalletStorage = .noOp
|
||||
|
||||
// Root of the test, the app finished the launch process and triggers the checks and initializations.
|
||||
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
|
||||
|
||||
await store.receive(.initialization(.initialSetups))
|
||||
|
||||
await store.receive(.initialization(.configureCrashReporter))
|
||||
|
||||
// initial attempt
|
||||
await store.receive(.initialization(.checkWalletInitialization))
|
||||
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
|
||||
state.appInitializationState = .keysMissing
|
||||
}
|
||||
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.keychainReadRetries = 1
|
||||
}
|
||||
|
||||
// 1st attempt
|
||||
await store.receive(.initialization(.checkWalletInitialization))
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing)))
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.keychainReadRetries = 2
|
||||
}
|
||||
|
||||
// 2nd attempt
|
||||
await store.receive(.initialization(.checkWalletInitialization))
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing)))
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.keychainReadRetries = 3
|
||||
}
|
||||
|
||||
// 3rd attempt
|
||||
await store.receive(.initialization(.checkWalletInitialization))
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing)))
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.alert = AlertState.walletStateFailed(.keysMissing)
|
||||
}
|
||||
|
||||
await store.receive(.destination(.updateDestination(.tabs))) { state in
|
||||
state.destinationState.internalDestination = .tabs
|
||||
state.destinationState.previousDestination = .welcome
|
||||
}
|
||||
|
||||
await store.finish()
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,21 @@ class RootTests: XCTestCase {
|
|||
|
||||
await store.send(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
|
||||
state.appInitializationState = .keysMissing
|
||||
state.alert = AlertState.walletStateFailed(.keysMissing)
|
||||
}
|
||||
|
||||
await store.receive(.initialization(.retryKeychainRead(.keysMissing))) { state in
|
||||
state.keychainReadRetries = 1
|
||||
}
|
||||
|
||||
await store.receive(.initialization(.checkWalletInitialization))
|
||||
|
||||
await store.receive(.initialization(.respondToWalletInitializationState(.uninitialized))) { state in
|
||||
state.appInitializationState = .uninitialized
|
||||
}
|
||||
|
||||
await store.receive(.destination(.updateDestination(.onboarding))) { state in
|
||||
state.destinationState.internalDestination = .onboarding
|
||||
state.destinationState.previousDestination = .welcome
|
||||
}
|
||||
|
||||
await store.finish()
|
||||
|
|
Loading…
Reference in New Issue