[#576] All the errors are handled by alert (#589)

Closes #576
This commit is contained in:
Michal Fousek 2023-03-02 09:02:24 +01:00 committed by GitHub
parent 23a616baab
commit f1c9b06123
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 177 additions and 39 deletions

View File

@ -22,7 +22,7 @@ struct MnemonicClient {
/// Generate deterministic seed from mnemonic phrase
var toSeed: (String) throws -> [UInt8]
/// Get this mnemonic phrase as array of words
var asWords: (String) throws -> [String]
var asWords: (String) -> [String]
/// Validates whether the given mnemonic is correct
var isValid: (String) throws -> Void
}

View File

@ -21,6 +21,7 @@ struct HomeReducer: ReducerProtocol {
case transactionHistory
}
@BindingState var alert: AlertState<HomeReducer.Action>?
var balanceBreakdownState: BalanceBreakdownReducer.State
var destination: Destination?
var profileState: ProfileReducer.State
@ -56,11 +57,12 @@ struct HomeReducer: ReducerProtocol {
enum Action: Equatable {
case balanceBreakdown(BalanceBreakdownReducer.Action)
case debugMenuStartup
case dismissAlert
case onAppear
case onDisappear
case profile(ProfileReducer.Action)
case request(RequestReducer.Action)
case rewindDone(Bool, SettingsReducer.Action)
case rewindDone(String?, SettingsReducer.Action)
case send(SendFlowReducer.Action)
case synchronizerStateChanged(SDKSynchronizerState)
case walletEvents(WalletEventsFlowReducer.Action)
@ -141,9 +143,9 @@ struct HomeReducer: ReducerProtocol {
return .run { send in
do {
try await sdkSynchronizer.rewind(.quick)
await send(.rewindDone(true, .quickRescan))
await send(.rewindDone(nil, .quickRescan))
} catch {
await send(.rewindDone(false, .quickRescan))
await send(.rewindDone(error.localizedDescription, .quickRescan))
}
}
@ -152,9 +154,9 @@ struct HomeReducer: ReducerProtocol {
return .run { send in
do {
try await sdkSynchronizer.rewind(.birthday)
await send(.rewindDone(true, .fullRescan))
await send(.rewindDone(nil, .fullRescan))
} catch {
await send(.rewindDone(false, .fullRescan))
await send(.rewindDone(error.localizedDescription, .fullRescan))
}
}
@ -164,8 +166,15 @@ struct HomeReducer: ReducerProtocol {
case .request:
return .none
case .rewindDone:
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
case let .rewindDone(errorDescription, _):
if let errorDescription {
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Rewind failed"),
message: TextState("Error: \(errorDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
return .none
case .walletEvents:
@ -186,6 +195,10 @@ struct HomeReducer: ReducerProtocol {
case .debugMenuStartup:
return .none
case .dismissAlert:
state.alert = nil
return .none
}
}
}

View File

@ -29,6 +29,7 @@ struct HomeView: View {
.navigationBarHidden(true)
.onAppear(perform: { viewStore.send(.onAppear) })
.onDisappear(perform: { viewStore.send(.onDisappear) })
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
.fullScreenCover(isPresented: viewStore.bindingForDestination(.balanceBreakdown)) {
BalanceBreakdownView(store: store.balanceBreakdownStore())
}

View File

@ -124,8 +124,8 @@ struct ImportWalletReducer: ReducerProtocol {
} catch {
// TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Wrong Seed Phrase"),
message: TextState("The seed phrase must be 24 words separated by space."),
title: TextState("Failed to restore wallet"),
message: TextState("Error: \(error.localizedDescription)"),
dismissButton: .default(
TextState("Ok"),
action: .send(.dismissAlert)

View File

@ -40,6 +40,7 @@ extension RootReducer {
case deeplink(URL)
case deeplinkHome
case deeplinkSend(Zatoshi, String, String)
case deeplinkFailed(URL, String)
case dismissAlert
case updateDestination(RootReducer.DestinationState.Destination)
}
@ -94,7 +95,7 @@ extension RootReducer {
)
)
} catch {
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
await send(.destination(.deeplinkFailed(url, error.localizedDescription)))
}
}
@ -111,6 +112,18 @@ extension RootReducer {
state.homeState.sendState.memoState.text = memo.redacted
return .none
case let .destination(.deeplinkFailed(url, errorDescription)):
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.destinationState.alert = AlertState(
title: TextState("Failed to process deeplink."),
message: TextState("Deeplink: \(url))\nError: \(errorDescription)"),
dismissButton: .default(
TextState("Ok"),
action: .send(.destination(.dismissAlert))
)
)
return .none
case .home(.walletEvents(.replyTo(let address))):
guard let url = URL(string: "zcash:\(address)") else {
return .none
@ -122,7 +135,7 @@ extension RootReducer {
return .none
case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation,
.sandbox, .welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded:
.sandbox, .welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .dismissAlert:
return .none
}

View File

@ -83,11 +83,22 @@ extension RootReducer {
case .initialization(.respondToWalletInitializationState(let walletState)):
switch walletState {
case .failed:
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("App initialisation state: failed"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
case .keysMissing:
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .keysMissing
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("App initialisation state: keysMissing."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
case .initialized, .filesMissing:
if walletState == .filesMissing {
state.appInitializationState = .filesMissing
@ -114,7 +125,12 @@ extension RootReducer {
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO: [#221] fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
// TODO: [#221] Handle fatal error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
return .none
}
@ -136,32 +152,37 @@ extension RootReducer {
try sdkSynchronizer.start()
return .none
} catch {
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
// TODO: [#221] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Failed to initialize the SDK"),
message: TextState("Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
return .none
}
case .initialization(.checkBackupPhraseValidation):
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO: [#221] fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
// TODO: [#221] Handle fatal error more properly (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
return .none
}
var landingDestination = RootReducer.DestinationState.Destination.home
if !storedWallet.hasUserPassedPhraseBackupTest && state.walletConfig.isEnabled(.testBackupPhraseFlow) {
do {
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value())
let recoveryPhrase = RecoveryPhrase(words: phraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
landingDestination = .phraseDisplay
} catch {
// TODO: [#201] - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
return .none
}
if !storedWallet.hasUserPassedPhraseBackupTest && state.walletConfig.isEnabled(.testBackupPhraseFlow) {
let phraseWords = mnemonic.asWords(storedWallet.seedPhrase.value())
let recoveryPhrase = RecoveryPhrase(words: phraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
landingDestination = .phraseDisplay
}
state.appInitializationState = .initialized
@ -181,7 +202,7 @@ extension RootReducer {
try walletStorage.importWallet(newRandomPhrase, birthday, .english, !state.walletConfig.isEnabled(.testBackupPhraseFlow))
// start the backup phrase validation test
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
let randomRecoveryPhraseWords = mnemonic.asWords(newRandomPhrase)
let recoveryPhrase = RecoveryPhrase(words: randomRecoveryPhraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
@ -192,6 +213,11 @@ extension RootReducer {
)
} catch {
// TODO: [#201] - merge with issue 221 (https://github.com/zcash/secant-ios-wallet/issues/221) and its Error States
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't create new wallet. Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
return .none
@ -201,6 +227,11 @@ extension RootReducer {
try walletStorage.markUserPassedPhraseBackupTest(true)
} catch {
// TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't store information that user passed phrase backup test. Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
return .none
@ -245,6 +276,12 @@ extension RootReducer {
case .nukeWalletFailed:
// TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
state.alert = AlertState(
title: TextState("Nuke of the wallet failed"),
message: TextState(""),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
let backDestination: EffectTask<RootReducer.Action>
if let previousDestination = state.destinationState.previousDestination {
backDestination = EffectTask(value: .destination(.updateDestination(previousDestination)))
@ -280,6 +317,10 @@ extension RootReducer {
)
return .none
case .dismissAlert:
state.alert = nil
return .none
case let .debug(.updateFlag(flag, isEnabled)):
return walletConfigProvider.update(flag, !isEnabled)
.receive(on: mainQueue)

View File

@ -10,6 +10,7 @@ struct RootReducer: ReducerProtocol {
enum WalletConfigCancelId {}
struct State: Equatable {
@BindingState var alert: AlertState<RootReducer.Action>?
var appInitializationState: InitializationState = .uninitialized
var destinationState: DestinationState
var homeState: HomeReducer.State
@ -25,6 +26,7 @@ struct RootReducer: ReducerProtocol {
enum Action: Equatable, BindableAction {
case binding(BindingAction<RootReducer.State>)
case debug(DebugAction)
case dismissAlert
case destination(DestinationAction)
case home(HomeReducer.Action)
case initialization(InitializationAction)

View File

@ -79,6 +79,7 @@ struct RootView: View {
}
}
.onOpenURL(perform: { viewStore.goToDeeplink($0) })
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
}
}
}

View File

@ -21,6 +21,7 @@ struct ScanReducer: ReducerProtocol {
case unknown
}
@BindingState var alert: AlertState<ScanReducer.Action>?
var isTorchAvailable = false
var isTorchOn = false
var isValidValue = false
@ -40,6 +41,7 @@ struct ScanReducer: ReducerProtocol {
@Dependency(\.uriParser) var uriParser
enum Action: Equatable {
case dismissAlert
case onAppear
case onDisappear
case found(RedactableString)
@ -51,6 +53,10 @@ struct ScanReducer: ReducerProtocol {
// swiftlint:disable:next cyclomatic_complexity
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask<Action> {
switch action {
case .dismissAlert:
state.alert = nil
return .none
case .onAppear:
// reset the values
state.scanStatus = .unknown
@ -60,7 +66,12 @@ struct ScanReducer: ReducerProtocol {
do {
state.isTorchAvailable = try captureDevice.isTorchAvailable()
} catch {
// TODO: [#322] handle torch errors (https://github.com/zcash/secant-ios-wallet/issues/322)
// TODO: [#322] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/322)
state.alert = AlertState(
title: TextState("Can't initialize the camera"),
message: TextState("Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
return .none
@ -105,6 +116,11 @@ struct ScanReducer: ReducerProtocol {
state.isTorchOn.toggle()
} catch {
// TODO: [#322] handle torch errors (https://github.com/zcash/secant-ios-wallet/issues/322)
state.alert = AlertState(
title: TextState("Can't initialize the camera"),
message: TextState("Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
return .none
}

View File

@ -50,6 +50,7 @@ struct ScanView: View {
.applyScreenBackground()
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
}
.ignoresSafeArea()
}

View File

@ -48,6 +48,7 @@ struct SettingsReducer: ReducerProtocol {
case exportLogs
case fullRescan
case logsExported
case logsExportFailed(String)
case logsShareFinished
case onAppear
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
@ -83,12 +84,17 @@ struct SettingsReducer: ReducerProtocol {
case .backupWallet:
do {
let storedWallet = try walletStorage.exportWallet()
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value())
let phraseWords = mnemonic.asWords(storedWallet.seedPhrase.value())
let recoveryPhrase = RecoveryPhrase(words: phraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
return EffectTask(value: .updateDestination(.backupPhrase))
} catch {
// TODO: [#221] - merge with issue 221 (https://github.com/zcash/secant-ios-wallet/issues/221) and its Error States
state.alert = AlertState(
title: TextState("Can't backup wallet"),
message: TextState("Error: \(error.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
return .none
}
@ -118,7 +124,7 @@ struct SettingsReducer: ReducerProtocol {
try await logsHandler.exportAndStoreLogs(state.tempSDKDir, state.tempTCADir, state.tempWalletDir)
await send(.logsExported)
} catch {
// TODO: [#527] address the error here https://github.com/zcash/secant-ios-wallet/issues/527
await send(.logsExportFailed(error.localizedDescription))
}
}
@ -127,6 +133,15 @@ struct SettingsReducer: ReducerProtocol {
state.isSharingLogs = true
return .none
case let .logsExportFailed(errorDescription):
// TODO: [#527] address the error here https://github.com/zcash/secant-ios-wallet/issues/527
state.alert = AlertState(
title: TextState("Error when exporting logs"),
message: TextState("Error: \(errorDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
return .none
case .logsShareFinished:
state.isSharingLogs = false
return .none

View File

@ -110,7 +110,7 @@ class HomeTests: XCTestCase {
state.destination = nil
}
await store.receive(.rewindDone(true, .quickRescan))
await store.receive(.rewindDone(nil, .quickRescan))
}
@MainActor func testFullRescan_ResetToHomeScreen() async throws {
@ -135,6 +135,6 @@ class HomeTests: XCTestCase {
state.destination = nil
}
await store.receive(.rewindDone(true, .fullRescan))
await store.receive(.rewindDone(nil, .fullRescan))
}
}

View File

@ -165,6 +165,11 @@ class AppInitializationTests: XCTestCase {
await store.receive(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
state.appInitializationState = .keysMissing
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("App initialisation state: keysMissing."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
await store.finish()

View File

@ -118,16 +118,23 @@ class RootTests: XCTestCase {
store.send(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
state.appInitializationState = .keysMissing
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("App initialisation state: keysMissing."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
}
func testRespondToWalletInitializationState_FilesMissing() throws {
let walletStorageError: Error = "export failed"
let store = TestStore(
initialState: .placeholder,
reducer: RootReducer()
) { dependencies in
dependencies.walletStorage = .noOp
dependencies.walletStorage.exportWallet = { throw "export failed" }
dependencies.walletStorage.exportWallet = { throw walletStorageError }
}
store.send(.initialization(.respondToWalletInitializationState(.filesMissing))) { state in
@ -137,18 +144,30 @@ class RootTests: XCTestCase {
store.receive(.initialization(.initializeSDK)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
state.alert = AlertState(
title: TextState("Failed to initialize the SDK"),
message: TextState("Error: \(walletStorageError.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
store.receive(.initialization(.checkBackupPhraseValidation))
store.receive(.initialization(.checkBackupPhraseValidation)) { state in
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
}
func testRespondToWalletInitializationState_Initialized() throws {
let walletStorageError: Error = "export failed"
let store = TestStore(
initialState: .placeholder,
reducer: RootReducer()
) { dependencies in
dependencies.walletStorage = .noOp
dependencies.walletStorage.exportWallet = { throw "export failed" }
dependencies.walletStorage.exportWallet = { throw walletStorageError }
}
store.send(.initialization(.respondToWalletInitializationState(.initialized)))
@ -156,9 +175,20 @@ class RootTests: XCTestCase {
store.receive(.initialization(.initializeSDK)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
state.alert = AlertState(
title: TextState("Failed to initialize the SDK"),
message: TextState("Error: \(walletStorageError.localizedDescription)"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
store.receive(.initialization(.checkBackupPhraseValidation))
store.receive(.initialization(.checkBackupPhraseValidation)) { state in
state.alert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
}
func testWalletEventReplyTo_validAddress() throws {