[#761] AlertReducer and RootReducer needs to be decoupled (#763)

- AlertRequest + Reducer + States removed
- Alerts decoupled and living within the features
- tests fixed
This commit is contained in:
Lukas Korba 2023-06-06 11:30:58 +02:00 committed by GitHub
parent 9b77a57a5f
commit e3a312ee1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 436 additions and 662 deletions

View File

@ -192,12 +192,6 @@
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */; };
9E2F1C8C280ED6A7004E65FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */; };
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8E280EDE09004E65FE /* Drawer.swift */; };
9E33ECD429D5D99000708DE4 /* AlertRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E33ECD029D4CCB600708DE4 /* AlertRequest.swift */; };
9E33ECD529D5D99700708DE4 /* AlertReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9CEA3229D32D5100599DF5 /* AlertReducer.swift */; };
9E33ECD629D5D99A00708DE4 /* AlertStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E33ECD229D4D1FB00708DE4 /* AlertStates.swift */; };
9E33ECD729D5E30200708DE4 /* AlertReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9CEA3229D32D5100599DF5 /* AlertReducer.swift */; };
9E33ECD829D5E30200708DE4 /* AlertStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E33ECD229D4D1FB00708DE4 /* AlertStates.swift */; };
9E33ECD929D5E30200708DE4 /* AlertRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E33ECD029D4CCB600708DE4 /* AlertRequest.swift */; };
9E33ECDA29D5E30700708DE4 /* OnChangeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9CEA3D29D47BE000599DF5 /* OnChangeReducer.swift */; };
9E34519529C4A4BF00177D16 /* AddressDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E207C382966EF87003E2C9B /* AddressDetailsTests.swift */; };
9E34519629C4A4D800177D16 /* secantUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A2526B364180058B01E /* secantUITests.swift */; };
@ -406,8 +400,6 @@
9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = "<group>"; };
9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
9E2F1C8E280EDE09004E65FE /* Drawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawer.swift; sourceTree = "<group>"; };
9E33ECD029D4CCB600708DE4 /* AlertRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertRequest.swift; sourceTree = "<group>"; };
9E33ECD229D4D1FB00708DE4 /* AlertStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStates.swift; sourceTree = "<group>"; };
9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportWalletTests.swift; sourceTree = "<group>"; };
9E39112D283F91600073DD9A /* ZatoshiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZatoshiTests.swift; sourceTree = "<group>"; };
9E391131284644580073DD9A /* AppInitializationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInitializationTests.swift; sourceTree = "<group>"; };
@ -449,7 +441,6 @@
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
9E9ADA7C2938F4C00071767B /* RootInitialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootInitialization.swift; sourceTree = "<group>"; };
9E9ADA7E2938F5EC0071767B /* RootDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootDestination.swift; sourceTree = "<group>"; };
9E9CEA3229D32D5100599DF5 /* AlertReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertReducer.swift; sourceTree = "<group>"; };
9E9CEA3D29D47BE000599DF5 /* OnChangeReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnChangeReducer.swift; sourceTree = "<group>"; };
9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSnapshotTests.swift; sourceTree = "<group>"; };
9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeSnapshotTests.swift; sourceTree = "<group>"; };
@ -598,13 +589,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0D0781C2278750C00083ACD7 /* Welcome */ = {
isa = PBXGroup;
children = (
);
path = Welcome;
sourceTree = "<group>";
};
0D4E79FC26B364170058B01E = {
isa = PBXGroup;
children = (
@ -822,20 +806,17 @@
isa = PBXGroup;
children = (
9E0031BD2A28B21A003DFCEB /* BalanceBreakdown */,
9E33ECCF29D4CC9900708DE4 /* Alerts */,
34C5657F29B60BDF002F3A7C /* ExportLogs */,
F93874EC273C4DE200F0E875 /* Home */,
9E2DF99727CF704D00649636 /* ImportWallet */,
3448CB3028E4764E006ADEDB /* NotEnoughFreeSpace */,
6654C73C2715A3FA00901167 /* OnboardingFlow */,
9E7FE0E4282E753700C374E8 /* RecoveryPhraseDisplay */,
F9971A4927680DC400A2DB75 /* Root */,
9EAFEB8B2808174900199FC9 /* Sandbox */,
F9971A5B27680DF600A2DB75 /* Scan */,
F9C165B62740403600592F76 /* SendFlow */,
F9971A6127680DFE00A2DB75 /* Settings */,
F96B41E2273B501F0021B49A /* WalletEventsFlow */,
0D0781C2278750C00083ACD7 /* Welcome */,
);
path = Features;
sourceTree = "<group>";
@ -938,16 +919,6 @@
path = Drawer;
sourceTree = "<group>";
};
9E33ECCF29D4CC9900708DE4 /* Alerts */ = {
isa = PBXGroup;
children = (
9E33ECD029D4CCB600708DE4 /* AlertRequest.swift */,
9E9CEA3229D32D5100599DF5 /* AlertReducer.swift */,
9E33ECD229D4D1FB00708DE4 /* AlertStates.swift */,
);
path = Alerts;
sourceTree = "<group>";
};
9E391122283E4C970073DD9A /* ImportWalletTests */ = {
isa = PBXGroup;
children = (
@ -1180,13 +1151,6 @@
path = "UI Components";
sourceTree = "<group>";
};
9E7FE0E4282E753700C374E8 /* RecoveryPhraseDisplay */ = {
isa = PBXGroup;
children = (
);
path = RecoveryPhraseDisplay;
sourceTree = "<group>";
};
9E7FE0E9282E7CF800C374E8 /* ImportSeedEditor */ = {
isa = PBXGroup;
children = (
@ -1878,7 +1842,6 @@
0D26AEDA299E8196005260EE /* ImportSeedEditor.swift in Sources */,
9E486DE629B637AF003E6945 /* ImportBirthdayView.swift in Sources */,
0D26AEDC299E8196005260EE /* CheckCircle.swift in Sources */,
9E33ECD929D5E30200708DE4 /* AlertRequest.swift in Sources */,
0D26AEE4299E8196005260EE /* CurrencySelectionView.swift in Sources */,
0D26AEE7299E8196005260EE /* TransactionAddressTextFieldStore.swift in Sources */,
34C5658629B60C8B002F3A7C /* ExportLogsStore.swift in Sources */,
@ -1919,7 +1882,6 @@
0D26AF3E299E8196005260EE /* SingleLineTextField.swift in Sources */,
0D26AF40299E8196005260EE /* RootDestination.swift in Sources */,
0D26AF41299E8196005260EE /* OnboardingProgressIndicator.swift in Sources */,
9E33ECD829D5E30200708DE4 /* AlertStates.swift in Sources */,
0D26AF44299E8196005260EE /* Memo+toString.swift in Sources */,
0D26AF46299E8196005260EE /* CheckCircleStore.swift in Sources */,
0D26AF47299E8196005260EE /* CreateTransactionView.swift in Sources */,
@ -1932,7 +1894,6 @@
9E0031C72A28BD54003DFCEB /* BalanceBreakdownView.swift in Sources */,
0D26AF56299E8196005260EE /* ScanStore.swift in Sources */,
0D26AF5F299E8196005260EE /* SendFlowView.swift in Sources */,
9E33ECD729D5E30200708DE4 /* AlertReducer.swift in Sources */,
0D26AF64299E8196005260EE /* SettingsStore.swift in Sources */,
0D26AF65299E8196005260EE /* InitializationState.swift in Sources */,
9E486DF429B9EEC4003E6945 /* UIResponder+Current.swift in Sources */,
@ -1946,14 +1907,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9E33ECD629D5D99A00708DE4 /* AlertStates.swift in Sources */,
2EB660E02747EAB900A06A07 /* OnboardingFlowView.swift in Sources */,
0D261040298C406F00CC9DE9 /* CrashReporterTestKey.swift in Sources */,
9EAFEB902808183D00199FC9 /* SandboxStore.swift in Sources */,
34DA414728E4385800F8CC61 /* TransactionSendingView.swift in Sources */,
F96B41E9273B501F0021B49A /* WalletEventsFlowView.swift in Sources */,
9E4AA4F829BF76BB00752BB3 /* About.swift in Sources */,
9E33ECD429D5D99000708DE4 /* AlertRequest.swift in Sources */,
0D26103E298C3FA600CC9DE9 /* CrashReporterLiveKey.swift in Sources */,
2EDA07A027EDE18C00D6F09B /* TCATextField.swift in Sources */,
2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */,
@ -1970,7 +1929,6 @@
9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */,
6654C73E2715A41300901167 /* OnboardingFlowStore.swift in Sources */,
2E6CF8DD27D78319004DCD7A /* CurrencySelectionStore.swift in Sources */,
9E33ECD529D5D99700708DE4 /* AlertReducer.swift in Sources */,
9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */,
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
9E486DE529B637AF003E6945 /* ImportBirthdayView.swift in Sources */,
@ -2576,7 +2534,7 @@
repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture";
requirement = {
kind = exactVersion;
version = 0.53.2;
version = 0.54.0;
};
};
9E2AC0FD27D8EC120042AA47 /* XCRemoteSwiftPackageReference "MnemonicSwift" */ = {

View File

@ -185,8 +185,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "b6559103c7867821b3848afe29afc1a386ae83f1",
"version" : "0.53.2"
"revision" : "8b98ba40a2bc8e70579397f906ceb325e7c04b2f",
"version" : "0.54.0"
}
},
{
@ -212,8 +212,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "25c9b6789b4b7ada649a3808e6d8de1489082a33",
"version" : "0.5.0"
"revision" : "de1a984a71e51f6e488e98ce3652035563eb8acb",
"version" : "0.5.1"
}
},
{
@ -311,8 +311,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation",
"state" : {
"revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6",
"version" : "0.7.1"
"revision" : "db81007362f998654239021ca9308a264e59d3e2",
"version" : "0.7.2"
}
},
{

View File

@ -1,118 +0,0 @@
//
// AlertReducer.swift
// secant
//
// Created by Lukáš Korba on 28.03.2023.
//
import ComposableArchitecture
extension ReducerProtocol<RootReducer.State, RootReducer.Action> {
func alerts() -> some ReducerProtocol<RootReducer.State, RootReducer.Action> {
UniAlert(base: self)
}
}
private struct UniAlert<Base: ReducerProtocol<RootReducer.State, RootReducer.Action>>: ReducerProtocol {
let base: Base
var body: some ReducerProtocol<RootReducer.State, RootReducer.Action> {
base
catchAlertRequests
passAlertActions
}
// Catching the alert side effects
@ReducerBuilder<State, Action>
var catchAlertRequests: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .alert(let alert):
state.uniAlert = alert.alertState()
return .none
case .exportLogs(.alert(let alert)):
state.uniAlert = alert.alertState()
return .none
case .home(.settings(.exportLogs(.alert(let alert)))):
state.uniAlert = alert.alertState()
return .none
case .home(.alert(let alert)):
state.uniAlert = alert.alertState()
return .none
case .home(.balanceBreakdown(.alert(let alert))):
state.uniAlert = alert.alertState()
return .none
case .home(.send(.scan(.alert(let alert)))):
state.uniAlert = alert.alertState()
return .none
case .onboarding(.importWallet(.alert(let alert))):
state.uniAlert = alert.alertState()
return .none
case .home(.settings(.alert(let alert))):
state.uniAlert = alert.alertState()
return .none
case .home(.walletEvents(.alert(let alert))):
state.uniAlert = alert.alertState()
return .none
default: return .none
}
}
}
// Passing alert actions back to the children
@ReducerBuilder<State, Action>
var passAlertActions: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .dismissAlert:
state.uniAlert = nil
return .none
case .uniAlert(.balanceBreakdown(let action)):
guard let action else { return .none }
return EffectTask(value: .home(.balanceBreakdown(action)))
case .uniAlert(.exportLogs(let action)):
guard let action else { return .none }
return .concatenate(
EffectTask(value: .exportLogs(action)),
EffectTask(value: .home(.settings(.exportLogs(action))))
)
case .uniAlert(.home(let action)):
guard let action else { return .none }
return EffectTask(value: .home(action))
case .uniAlert(.importWallet(let action)):
guard let action else { return .none }
return EffectTask(value: .onboarding(.importWallet(action)))
case .uniAlert(.root(let action)):
guard let action else { return .none }
return EffectTask(value: action)
case .uniAlert(.scan(let action)):
guard let action else { return .none }
return EffectTask(value: .home(.send(.scan(action))))
case .uniAlert(.settings(let action)):
guard let action else { return .none }
return EffectTask(value: .home(.settings(action)))
case .uniAlert(.walletEvents(let action)):
guard let action else { return .none }
return EffectTask(value: .home(.walletEvents(action)))
default: return .none
}
}
}
}

View File

@ -1,99 +0,0 @@
//
// AlertRequest.swift
// secant-testnet
//
// Created by Lukáš Korba on 29.03.2023.
//
import Foundation
import ComposableArchitecture
import ZcashLightClientKit
extension RootReducer {
indirect enum AlertAction: Equatable {
case balanceBreakdown(BalanceBreakdownReducer.Action?)
case exportLogs(ExportLogsReducer.Action?)
case home(HomeReducer.Action?)
case importWallet(ImportWalletReducer.Action?)
case root(RootReducer.Action?)
case scan(ScanReducer.Action?)
case settings(SettingsReducer.Action?)
case walletEvents(WalletEventsFlowReducer.Action?)
}
}
enum AlertRequest: Equatable {
enum BalanceBreakdown: Equatable {
case shieldFundsSuccess
case shieldFundsFailure(ZcashError)
}
enum ExportLogs: Equatable {
case failed(ZcashError)
}
enum Home: Equatable {
case syncFailed(ZcashError, String)
}
enum ImportWallet: Equatable {
case succeed
case failed(ZcashError)
}
enum Root: Equatable {
case cantCreateNewWallet(ZcashError)
case cantLoadSeedPhrase
case cantStartSync(ZcashError)
case cantStoreThatUserPassedPhraseBackupTest(ZcashError)
case failedToProcessDeeplink(URL, ZcashError)
case initializationFailed(ZcashError)
case rewindFailed(ZcashError)
case walletStateFailed(InitializationState)
case wipeFailed
case wipeRequest
}
enum Scan: Equatable {
case cantInitializeCamera(ZcashError)
}
enum Settings: Equatable {
case cantBackupWallet(ZcashError)
case sendSupportMail
}
enum WalletEvents: Equatable {
case warnBeforeLeavingApp(URL?)
}
case balanceBreakdown(BalanceBreakdown)
case exportLogs(ExportLogs)
case home(Home)
case importWallet(ImportWallet)
case root(Root)
case scan(Scan)
case settings(Settings)
case walletEvents(WalletEvents)
func alertState() -> AlertState<RootReducer.Action> {
switch self {
case .balanceBreakdown(let balanceBreakdown):
return balanceBreakdownAlertState(balanceBreakdown)
case .exportLogs(let exportLogs):
return exportLogsAlertState(exportLogs)
case .home(let home):
return homeAlertState(home)
case .importWallet(let importWallet):
return importWalletAlertState(importWallet)
case .root(let root):
return rootAlertState(root)
case .scan(let scan):
return scanAlertState(scan)
case .settings(let settings):
return settingsAlertState(settings)
case .walletEvents(let walletEvents):
return walletEventsAlertState(walletEvents)
}
}
}

View File

@ -1,214 +0,0 @@
//
// AlertStates.swift
// secant-testnet
//
// Created by Lukáš Korba on 29.03.2023.
//
import ComposableArchitecture
import Generated
// MARK: - Balance Breakdown
extension AlertRequest {
func balanceBreakdownAlertState(_ balanceBreakdown: BalanceBreakdown) -> AlertState<RootReducer.Action> {
switch balanceBreakdown {
case .shieldFundsFailure(let error):
return AlertState(
title: TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Failure.title),
message: TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Failure.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .shieldFundsSuccess:
return AlertState(
title: TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Success.title),
message: TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Success.message),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
}
}
}
// MARK: - Export Logs
extension AlertRequest {
func exportLogsAlertState(_ exportLogs: ExportLogs) -> AlertState<RootReducer.Action> {
switch exportLogs {
case .failed(let error):
return AlertState(
title: TextState(L10n.ExportLogs.Alert.Failed.title),
message: TextState(L10n.ExportLogs.Alert.Failed.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
}
}
}
// MARK: - Home
extension AlertRequest {
func homeAlertState(_ home: Home) -> AlertState<RootReducer.Action> {
switch home {
case let .syncFailed(error, secondaryButtonTitle):
return AlertState(
title: TextState(L10n.Home.SyncFailed.title),
message: TextState("\(error.message) (code: \(error.code.rawValue))"),
primaryButton: .default(TextState(L10n.Home.SyncFailed.retry), action: .send(.uniAlert(.home(.retrySync)))),
secondaryButton: .default(TextState(secondaryButtonTitle), action: .send(.dismissAlert))
)
}
}
}
// MARK: - Import Wallet
extension AlertRequest {
func importWalletAlertState(_ importWallet: ImportWallet) -> AlertState<RootReducer.Action> {
switch importWallet {
case .succeed:
return AlertState(
title: TextState(L10n.General.success),
message: TextState(L10n.ImportWallet.Alert.Success.message),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.uniAlert(.importWallet(.successfullyRecovered))))
)
case .failed(let error):
return AlertState(
title: TextState(L10n.ImportWallet.Alert.Failed.title),
message: TextState(L10n.ImportWallet.Alert.Failed.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
}
}
}
// MARK: - Root
extension AlertRequest {
func rootAlertState(_ root: Root) -> AlertState<RootReducer.Action> {
switch root {
case .cantCreateNewWallet(let error):
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Failed.title),
message: TextState(L10n.Root.Initialization.Alert.CantCreateNewWallet.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .cantLoadSeedPhrase:
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Failed.title),
message: TextState(L10n.Root.Initialization.Alert.CantLoadSeedPhrase.message),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .cantStartSync(let error):
return AlertState(
title: TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.title),
message: TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .cantStoreThatUserPassedPhraseBackupTest(let error):
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Failed.title),
message: TextState(
L10n.Root.Initialization.Alert.CantStoreThatUserPassedPhraseBackupTest.message(error.message, error.code.rawValue)
),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case let .failedToProcessDeeplink(url, error):
return AlertState(
title: TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.title),
message: TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.message(url, error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .initializationFailed(let error):
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.SdkInitFailed.title),
message: TextState(L10n.Root.Initialization.Alert.Error.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .rewindFailed(let error):
return AlertState(
title: TextState(L10n.Root.Debug.Alert.Rewind.Failed.title),
message: TextState(L10n.Root.Debug.Alert.Rewind.Failed.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .walletStateFailed(let walletState):
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Failed.title),
message: TextState(L10n.Root.Initialization.Alert.WalletStateFailed.message(walletState)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .wipeFailed:
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.WipeFailed.title),
message: TextState(""),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .wipeRequest:
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Wipe.title),
message: TextState(L10n.Root.Initialization.Alert.Wipe.message),
buttons: [
.destructive(TextState(L10n.General.yes), action: .send(.initialization(.nukeWallet))),
.cancel(TextState(L10n.General.no), action: .send(.dismissAlert))
]
)
}
}
}
// MARK: - Scan
extension AlertRequest {
func scanAlertState(_ scan: Scan) -> AlertState<RootReducer.Action> {
switch scan {
case .cantInitializeCamera(let error):
return AlertState(
title: TextState(L10n.Scan.Alert.CantInitializeCamera.title),
message: TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
}
}
}
// MARK: - Settings
extension AlertRequest {
func settingsAlertState(_ settings: Settings) -> AlertState<RootReducer.Action> {
switch settings {
case .cantBackupWallet(let error):
return AlertState<RootReducer.Action>(
title: TextState(L10n.Settings.Alert.CantBackupWallet.title),
message: TextState(L10n.Settings.Alert.CantBackupWallet.message(error.message, error.code.rawValue)),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case .sendSupportMail:
return AlertState<RootReducer.Action>(
title: TextState(L10n.Settings.Alert.CantSendEmail.title),
message: TextState(L10n.Settings.Alert.CantSendEmail.message),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.uniAlert(.settings(.sendSupportMailFinished))))
)
}
}
}
// MARK: - Wallet Events
extension AlertRequest {
func walletEventsAlertState(_ walletEvents: WalletEvents) -> AlertState<RootReducer.Action> {
switch walletEvents {
case .warnBeforeLeavingApp(let blockExplorerURL):
return AlertState(
title: TextState(L10n.WalletEvent.Alert.LeavingApp.title),
message: TextState(L10n.WalletEvent.Alert.LeavingApp.message),
primaryButton: .cancel(
TextState(L10n.WalletEvent.Alert.LeavingApp.Button.nevermind),
action: .send(.dismissAlert)
),
secondaryButton: .default(
TextState(L10n.WalletEvent.Alert.LeavingApp.Button.seeOnline),
action: .send(.uniAlert(.walletEvents(.openBlockExplorer(blockExplorerURL))))
)
)
}
}
}

View File

@ -23,6 +23,7 @@ struct BalanceBreakdownReducer: ReducerProtocol {
private enum CancelId { case timer }
struct State: Equatable {
@PresentationState var alert: AlertState<Action>?
var autoShieldingThreshold: Zatoshi
var latestBlock: String
var shieldedBalance: Balance
@ -43,7 +44,7 @@ struct BalanceBreakdownReducer: ReducerProtocol {
}
enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case onAppear
case onDisappear
case shieldFunds
@ -94,11 +95,13 @@ struct BalanceBreakdownReducer: ReducerProtocol {
case .shieldFundsSuccess:
state.shieldingFunds = false
return EffectTask(value: .alert(.balanceBreakdown(.shieldFundsSuccess)))
state.alert = AlertState.shieldFundsSuccess()
return .none
case .shieldFundsFailure(let error):
state.shieldingFunds = false
return EffectTask(value: .alert(.balanceBreakdown(.shieldFundsFailure(error))))
state.alert = AlertState.shieldFundsFailure(error)
return .none
case .synchronizerStateChanged(let latestState):
state.shieldedBalance = latestState.shieldedBalance.redacted
@ -108,13 +111,33 @@ struct BalanceBreakdownReducer: ReducerProtocol {
case .updateLatestBlock:
let latestBlockNumber = sdkSynchronizer.latestScannedHeight()
let latestBlock = numberFormatter.string(NSDecimalNumber(value: latestBlockNumber))
state.latestBlock = "\(String(describing: latestBlock))"
state.latestBlock = "\(String(describing: latestBlock ?? ""))"
return .none
}
}
}
}
// MARK: Alerts
extension AlertState where Action == BalanceBreakdownReducer.Action {
static func shieldFundsFailure(_ error: ZcashError) -> AlertState<BalanceBreakdownReducer.Action> {
AlertState<BalanceBreakdownReducer.Action> {
TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Failure.title)
} message: {
TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Failure.message(error.message, error.code.rawValue))
}
}
static func shieldFundsSuccess() -> AlertState<BalanceBreakdownReducer.Action> {
AlertState<BalanceBreakdownReducer.Action> {
TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Success.title)
} message: {
TextState(L10n.BalanceBreakdown.Alert.ShieldFunds.Success.message)
}
}
}
// MARK: - Placeholders
extension BalanceBreakdownReducer.State {

View File

@ -52,6 +52,10 @@ struct BalanceBreakdownView: View {
.onDisappear { viewStore.send(.onDisappear) }
}
.applyScreenBackground()
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
}
}

View File

@ -11,19 +11,21 @@ import Foundation
import ZcashLightClientKit
import LogsHandler
import Utils
import Generated
typealias ExportLogsStore = Store<ExportLogsReducer.State, ExportLogsReducer.Action>
typealias ExportLogsViewStore = ViewStore<ExportLogsReducer.State, ExportLogsReducer.Action>
struct ExportLogsReducer: ReducerProtocol {
struct State: Equatable {
@PresentationState var alert: AlertState<Action>?
var exportLogsDisabled = false
var isSharingLogs = false
var zippedLogsURLs: [URL] = []
}
indirect enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case start
case finished(URL?)
case failed(ZcashError)
@ -64,7 +66,8 @@ struct ExportLogsReducer: ReducerProtocol {
case let .failed(error):
state.exportLogsDisabled = false
state.isSharingLogs = false
return EffectTask(value: .alert(.exportLogs(.failed(error))))
state.alert = AlertState.failed(error)
return .none
case .shareFinished:
state.isSharingLogs = false
@ -74,6 +77,18 @@ struct ExportLogsReducer: ReducerProtocol {
}
}
// MARK: Alerts
extension AlertState where Action == ExportLogsReducer.Action {
static func failed(_ error: ZcashError) -> AlertState<ExportLogsReducer.Action> {
AlertState<ExportLogsReducer.Action> {
TextState(L10n.ExportLogs.Alert.Failed.title)
} message: {
TextState(L10n.ExportLogs.Alert.Failed.message(error.message, error.code.rawValue))
}
}
}
// MARK: Placeholders
extension ExportLogsReducer.State {

View File

@ -27,6 +27,7 @@ struct HomeReducer: ReducerProtocol {
case transactionHistory
}
@PresentationState var alert: AlertState<Action>?
var balanceBreakdownState: BalanceBreakdownReducer.State
var destination: Destination?
var canRequestReview = false
@ -68,24 +69,25 @@ struct HomeReducer: ReducerProtocol {
}
enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case balanceBreakdown(BalanceBreakdownReducer.Action)
case debugMenuStartup
case dismissAlert
case foundTransactions
case onAppear
case onDisappear
case profile(ProfileReducer.Action)
case resolveReviewRequest
case retrySync
case reviewRequestFinished
case send(SendFlowReducer.Action)
case settings(SettingsReducer.Action)
case syncFailed(ZcashError)
case foundTransactions
case synchronizerStateChanged(SynchronizerState)
case walletEvents(WalletEventsFlowReducer.Action)
case updateDestination(HomeReducer.State.Destination?)
case showSynchronizerErrorAlert(ZcashError)
case retrySync
case synchronizerStateChanged(SynchronizerState)
case syncFailed(ZcashError)
case updateDestination(HomeReducer.State.Destination?)
case updateWalletEvents([WalletEvent])
case walletEvents(WalletEventsFlowReducer.Action)
}
@Dependency(\.audioServices) var audioServices
@ -214,7 +216,8 @@ struct HomeReducer: ReducerProtocol {
}
case .showSynchronizerErrorAlert(let error):
return EffectTask(value: .alert(.home(.syncFailed(error, L10n.Home.SyncFailed.dismiss))))
state.alert = AlertState.syncFailed(error, L10n.Home.SyncFailed.dismiss)
return .none
case .balanceBreakdown(.onDisappear):
state.destination = nil
@ -227,10 +230,14 @@ struct HomeReducer: ReducerProtocol {
return .none
case .syncFailed(let error):
return EffectTask(value: .alert(.home(.syncFailed(error, L10n.General.ok))))
state.alert = AlertState.syncFailed(error, L10n.General.ok)
return .none
case .alert:
return .none
case .dismissAlert:
return .none
}
}
}
@ -288,6 +295,25 @@ extension HomeViewStore {
}
}
// MARK: Alerts
extension AlertState where Action == HomeReducer.Action {
static func syncFailed(_ error: ZcashError, _ secondaryButtonTitle: String) -> AlertState<HomeReducer.Action> {
AlertState<HomeReducer.Action> {
TextState(L10n.Home.SyncFailed.title)
} actions: {
ButtonState(action: .retrySync) {
TextState(L10n.Home.SyncFailed.retry)
}
ButtonState(action: .dismissAlert) {
TextState(secondaryButtonTitle)
}
} message: {
TextState("\(error.message) (code: \(error.code.rawValue))")
}
}
}
// MARK: Placeholders
extension HomeReducer.State {

View File

@ -42,6 +42,10 @@ struct HomeView: View {
}
}
.onDisappear { viewStore.send(.onDisappear) }
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.balanceBreakdown),
destination: { BalanceBreakdownView(store: store.balanceBreakdownStore()) }

View File

@ -21,6 +21,7 @@ struct ImportWalletReducer: ReducerProtocol {
case birthday
}
@PresentationState var alert: AlertState<Action>?
var birthdayHeight = "".redacted
var birthdayHeightValue: RedactableBlockHeight?
var destination: Destination?
@ -46,8 +47,9 @@ struct ImportWalletReducer: ReducerProtocol {
}
enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case birthdayInputChanged(RedactableString)
case dismissAlert
case restoreWallet
case importPrivateOrViewingKey
case initializeSDK
@ -111,15 +113,15 @@ struct ImportWalletReducer: ReducerProtocol {
// update the backup phrase validation flag
try walletStorage.markUserPassedPhraseBackupTest(true)
state.alert = AlertState.succeed()
// notify user
return .concatenate(
EffectTask(value: .alert(.importWallet(.succeed))),
EffectTask(value: .initializeSDK)
)
return EffectTask(value: .initializeSDK)
} catch {
return EffectTask(value: .alert(.importWallet(.failed(error.toZcashError()))))
state.alert = AlertState.failed(error.toZcashError())
}
return .none
case .updateDestination(let destination):
state.destination = destination
return .none
@ -132,6 +134,9 @@ struct ImportWalletReducer: ReducerProtocol {
case .initializeSDK:
return .none
case .dismissAlert:
return .none
}
}
}
@ -164,6 +169,34 @@ extension ImportWalletViewStore {
}
}
// MARK: Alerts
extension AlertState where Action == ImportWalletReducer.Action {
static func succeed() -> AlertState<ImportWalletReducer.Action> {
AlertState<ImportWalletReducer.Action> {
TextState(L10n.General.success)
} actions: {
ButtonState(action: .successfullyRecovered) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.ImportWallet.Alert.Success.message)
}
}
static func failed(_ error: ZcashError) -> AlertState<ImportWalletReducer.Action> {
AlertState<ImportWalletReducer.Action> {
TextState(L10n.ImportWallet.Alert.Failed.title)
} actions: {
ButtonState(action: .dismissAlert) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.ImportWallet.Alert.Failed.message(error.message, error.code.rawValue))
}
}
}
// MARK: - Placeholders
extension ImportWalletReducer.State {

View File

@ -44,6 +44,10 @@ struct ImportWalletView: View {
isActive: viewStore.bindingForDestination(.birthday),
destination: { ImportBirthdayView(store: store) }
)
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
}
}
}

View File

@ -67,7 +67,8 @@ extension RootReducer {
case let .debug(.rewindDone(error, _)):
if let error {
return EffectTask(value: .alert(.root(.rewindFailed(error.toZcashError()))))
state.alert = AlertState.rewindFailed(error.toZcashError())
return .none
} else {
return .run { send in
do {
@ -96,7 +97,8 @@ extension RootReducer {
return EffectTask(value: .updateStateAfterConfigUpdate(walletConfig))
case .debug(.cantStartSync(let error)):
return EffectTask(value: .alert(.root(.cantStartSync(error))))
state.alert = AlertState.cantStartSync(error)
return .none
case .debug(.rateTheApp):
return .none

View File

@ -113,7 +113,8 @@ extension RootReducer {
return .none
case let .destination(.deeplinkFailed(url, error)):
return EffectTask(value: .alert(.root(.failedToProcessDeeplink(url, error))))
state.alert = AlertState.failedToProcessDeeplink(url, error)
return .none
case .home(.walletEvents(.replyTo(let address))):
guard let url = URL(string: "zcash:\(address)") else {
@ -122,7 +123,7 @@ extension RootReducer {
return EffectTask(value: .destination(.deeplink(url)))
case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .updateStateAfterConfigUpdate, .alert,
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .dismissAlert, .exportLogs, .uniAlert:
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .dismissAlert, .exportLogs:
return .none
}

View File

@ -85,10 +85,12 @@ extension RootReducer {
switch walletState {
case .failed:
state.appInitializationState = .failed
return EffectTask(value: .alert(.root(.walletStateFailed(walletState))))
state.alert = AlertState.walletStateFailed(walletState)
return .none
case .keysMissing:
state.appInitializationState = .keysMissing
return EffectTask(value: .alert(.root(.walletStateFailed(walletState))))
state.alert = AlertState.walletStateFailed(walletState)
return .none
case .initialized, .filesMissing:
if walletState == .filesMissing {
state.appInitializationState = .filesMissing
@ -113,7 +115,8 @@ extension RootReducer {
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
return EffectTask(value: .alert(.root(.cantLoadSeedPhrase)))
state.alert = AlertState.cantLoadSeedPhrase()
return .none
}
let birthday = state.storedWallet?.birthday?.value() ?? zcashSDKEnvironment.latestCheckpoint(TargetConstants.zcashNetwork)
@ -138,7 +141,8 @@ extension RootReducer {
case .initialization(.checkBackupPhraseValidation):
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
return EffectTask(value: .alert(.root(.cantLoadSeedPhrase)))
state.alert = AlertState.cantLoadSeedPhrase()
return .none
}
var landingDestination = RootReducer.DestinationState.Destination.home
@ -179,19 +183,21 @@ extension RootReducer {
EffectTask(value: .phraseValidation(.displayBackedUpPhrase))
)
} catch {
return EffectTask(value: .alert(.root(.cantCreateNewWallet(error.toZcashError()))))
state.alert = AlertState.cantCreateNewWallet(error.toZcashError())
}
return .none
case .phraseValidation(.succeed):
do {
try walletStorage.markUserPassedPhraseBackupTest(true)
return .none
} catch {
return EffectTask(value: .alert(.root(.cantStoreThatUserPassedPhraseBackupTest(error.toZcashError()))))
state.alert = AlertState.cantStoreThatUserPassedPhraseBackupTest(error.toZcashError())
}
return .none
case .initialization(.nukeWalletRequest):
return EffectTask(value: .alert(.root(.wipeRequest)))
state.alert = AlertState.wipeRequest()
return .none
case .initialization(.nukeWallet):
guard let wipePublisher = sdkSynchronizer.wipe() else {
@ -221,8 +227,8 @@ extension RootReducer {
} else {
backDestination = EffectTask(value: .destination(.updateDestination(state.destinationState.destination)))
}
state.alert = AlertState.wipeFailed()
return .concatenate(
EffectTask(value: .alert(.root(.wipeFailed))),
.cancel(id: SynchronizerCancelId.timer),
backDestination
)
@ -256,10 +262,11 @@ extension RootReducer {
case .initialization(.initializationFailed(let error)):
state.appInitializationState = .failed
return EffectTask(value: .alert(.root(.initializationFailed(error))))
state.alert = AlertState.initializationFailed(error)
return .none
case .home, .destination, .onboarding, .phraseDisplay, .phraseValidation, .sandbox,
.welcome, .binding, .debug, .exportLogs, .uniAlert, .dismissAlert, .alert:
.welcome, .binding, .debug, .exportLogs, .dismissAlert, .alert:
return .none
}
}

View File

@ -10,6 +10,8 @@ import UserPreferencesStorage
import Models
import RecoveryPhraseDisplay
import Welcome
import Generated
import Foundation
typealias RootStore = Store<RootReducer.State, RootReducer.Action>
typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>
@ -20,6 +22,7 @@ struct RootReducer: ReducerProtocol {
enum WalletConfigCancelId { case timer }
struct State: Equatable {
@PresentationState var alert: AlertState<Action>?
var appInitializationState: InitializationState = .uninitialized
var debugState: DebugState
var destinationState: DestinationState
@ -30,13 +33,12 @@ struct RootReducer: ReducerProtocol {
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var sandboxState: SandboxReducer.State
var storedWallet: StoredWallet?
@BindingState var uniAlert: AlertState<RootReducer.Action>?
var walletConfig: WalletConfig
var welcomeState: WelcomeReducer.State
}
enum Action: Equatable, BindableAction {
case alert(AlertRequest)
enum Action: Equatable {
case alert(PresentationAction<Action>)
case binding(BindingAction<RootReducer.State>)
case debug(DebugAction)
case dismissAlert
@ -50,7 +52,6 @@ struct RootReducer: ReducerProtocol {
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case phraseValidation(RecoveryPhraseValidationFlowReducer.Action)
case sandbox(SandboxReducer.Action)
case uniAlert(AlertAction)
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(WelcomeReducer.Action)
@ -71,8 +72,6 @@ struct RootReducer: ReducerProtocol {
@ReducerBuilder<State, Action>
var core: some ReducerProtocol<State, Action> {
BindingReducer()
Scope(state: \.homeState, action: /Action.home) {
HomeReducer()
}
@ -110,7 +109,6 @@ struct RootReducer: ReducerProtocol {
var body: some ReducerProtocol<State, Action> {
self.core
.alerts()
}
}
@ -148,6 +146,97 @@ extension RootReducer {
}
}
// MARK: Alerts
extension AlertState where Action == RootReducer.Action {
static func cantCreateNewWallet(_ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.CantCreateNewWallet.message(error.message, error.code.rawValue))
}
}
static func cantLoadSeedPhrase() -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.CantLoadSeedPhrase.message)
}
}
static func cantStartSync(_ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.title)
} message: {
TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.message(error.message, error.code.rawValue))
}
}
static func cantStoreThatUserPassedPhraseBackupTest(_ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(
L10n.Root.Initialization.Alert.CantStoreThatUserPassedPhraseBackupTest.message(error.message, error.code.rawValue)
)
}
}
static func failedToProcessDeeplink(_ url: URL, _ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.title)
} message: {
TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.message(url, error.message, error.code.rawValue))
}
}
static func initializationFailed(_ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.SdkInitFailed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.Error.message(error.message, error.code.rawValue))
}
}
static func rewindFailed(_ error: ZcashError) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Debug.Alert.Rewind.Failed.title)
} message: {
TextState(L10n.Root.Debug.Alert.Rewind.Failed.message(error.message, error.code.rawValue))
}
}
static func walletStateFailed(_ walletState: InitializationState) -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.WalletStateFailed.message(walletState))
}
}
static func wipeFailed() -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.WipeFailed.title)
}
}
static func wipeRequest() -> AlertState<RootReducer.Action> {
AlertState<RootReducer.Action> {
TextState(L10n.Root.Initialization.Alert.Wipe.title)
} actions: {
ButtonState(role: .destructive, action: .initialization(.nukeWallet)) {
TextState(L10n.General.yes)
}
ButtonState(role: .cancel, action: .dismissAlert) {
TextState(L10n.General.no)
}
} message: {
TextState(L10n.Root.Initialization.Alert.Wipe.message)
}
}
}
// MARK: Placeholders
extension RootReducer.State {

View File

@ -114,11 +114,15 @@ private extension RootView {
}
}
.onOpenURL(perform: { viewStore.goToDeeplink($0) })
.alert(store.scope(
state: \.uniAlert,
action: { _ in RootReducer.Action.dismissAlert }
), dismiss: .dismissAlert)
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
.alert(store: store.scope(
state: \.exportLogsState.$alert,
action: { .exportLogs(.alert($0)) }
))
shareLogsView(viewStore)
}
}

View File

@ -10,6 +10,8 @@ import Foundation
import CaptureDevice
import Utils
import URIParser
import ZcashLightClientKit
import Generated
typealias ScanStore = Store<ScanReducer.State, ScanReducer.Action>
typealias ScanViewStore = ViewStore<ScanReducer.State, ScanReducer.Action>
@ -24,6 +26,7 @@ struct ScanReducer: ReducerProtocol {
case unknown
}
@PresentationState var alert: AlertState<Action>?
var isTorchAvailable = false
var isTorchOn = false
var scanStatus: ScanStatus = .unknown
@ -49,7 +52,7 @@ struct ScanReducer: ReducerProtocol {
@Dependency(\.uriParser) var uriParser
enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case onAppear
case onDisappear
case found(RedactableString)
@ -59,63 +62,78 @@ struct ScanReducer: ReducerProtocol {
}
// swiftlint:disable:next cyclomatic_complexity
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask<Action> {
switch action {
case .alert:
return .none
case .onAppear:
// reset the values
state.scanStatus = .unknown
state.isTorchOn = false
// check the torch availability
do {
state.isTorchAvailable = try captureDevice.isTorchAvailable()
var body: some ReducerProtocolOf<Self> {
Reduce { state, action in
switch action {
case .alert:
return .none
} catch {
return EffectTask(value: .alert(.scan(.cantInitializeCamera(error.toZcashError()))))
}
case .onDisappear:
return .cancel(id: CancelId.timer)
case .found:
return .none
case .scanFailed:
state.scanStatus = .failed
return .none
case .scan(let code):
// the logic for the same scanned code is skipped until some new code
if let prevCode = state.scannedValue, prevCode == code.data {
case .onAppear:
// reset the values
state.scanStatus = .unknown
state.isTorchOn = false
// check the torch availability
do {
state.isTorchAvailable = try captureDevice.isTorchAvailable()
} catch {
state.alert = AlertState.cantInitializeCamera(error.toZcashError())
}
return .none
}
if uriParser.isValidURI(code.data, TargetConstants.zcashNetwork.networkType) {
state.scanStatus = .value(code)
// once valid URI is scanned we want to start the timer to deliver the code
// any new code cancels the schedule and fires new one
return .concatenate(
EffectTask.cancel(id: CancelId.timer),
EffectTask(value: .found(code))
.delay(for: 1.0, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.timer, cancelInFlight: true)
)
} else {
case .onDisappear:
return .cancel(id: CancelId.timer)
case .found:
return .none
case .scanFailed:
state.scanStatus = .failed
}
return .cancel(id: CancelId.timer)
case .torchPressed:
do {
try captureDevice.torch(!state.isTorchOn)
state.isTorchOn.toggle()
return .none
} catch {
return EffectTask(value: .alert(.scan(.cantInitializeCamera(error.toZcashError()))))
case .scan(let code):
// the logic for the same scanned code is skipped until some new code
if let prevCode = state.scannedValue, prevCode == code.data {
return .none
}
if uriParser.isValidURI(code.data, TargetConstants.zcashNetwork.networkType) {
state.scanStatus = .value(code)
// once valid URI is scanned we want to start the timer to deliver the code
// any new code cancels the schedule and fires new one
return .concatenate(
EffectTask.cancel(id: CancelId.timer),
EffectTask(value: .found(code))
.delay(for: 1.0, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.timer, cancelInFlight: true)
)
} else {
state.scanStatus = .failed
}
return .cancel(id: CancelId.timer)
case .torchPressed:
do {
try captureDevice.torch(!state.isTorchOn)
state.isTorchOn.toggle()
} catch {
state.alert = AlertState.cantInitializeCamera(error.toZcashError())
}
return .none
}
}
.ifLet(\.$alert, action: /Action.alert)
}
}
// MARK: Alerts
extension AlertState where Action == ScanReducer.Action {
static func cantInitializeCamera(_ error: ZcashError) -> AlertState<ScanReducer.Action> {
AlertState<ScanReducer.Action> {
TextState(L10n.Scan.Alert.CantInitializeCamera.title)
} message: {
TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.message, error.code.rawValue))
}
}
}

View File

@ -54,6 +54,10 @@ struct ScanView: View {
}
.ignoresSafeArea()
}
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
}
}

View File

@ -8,6 +8,8 @@ import LocalAuthenticationHandler
import SupportDataGenerator
import Models
import RecoveryPhraseDisplay
import ZcashLightClientKit
import Generated
typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action>
typealias SettingsViewStore = ViewStore<SettingsReducer.State, SettingsReducer.Action>
@ -19,6 +21,7 @@ struct SettingsReducer: ReducerProtocol {
case backupPhrase
}
@PresentationState var alert: AlertState<Action>?
var appVersion = ""
var appBuild = ""
var destination: Destination?
@ -29,7 +32,7 @@ struct SettingsReducer: ReducerProtocol {
}
enum Action: BindableAction, Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case backupWallet
case backupWalletAccessRequest
case binding(BindingAction<SettingsReducer.State>)
@ -73,9 +76,10 @@ struct SettingsReducer: ReducerProtocol {
state.phraseDisplayState.phrase = recoveryPhrase
return EffectTask(value: .updateDestination(.backupPhrase))
} catch {
return EffectTask(value: .alert(.settings(.cantBackupWallet(error.toZcashError()))))
state.alert = AlertState.cantBackupWallet(error.toZcashError())
}
return .none
case .binding(\.$isCrashReportingOn):
if state.isCrashReportingOn {
crashReporter.optOut()
@ -104,10 +108,10 @@ struct SettingsReducer: ReducerProtocol {
case .sendSupportMail:
if MFMailComposeViewController.canSendMail() {
state.supportData = SupportDataGenerator.generate()
return .none
} else {
return EffectTask(value: .alert(.settings(.sendSupportMail)))
state.alert = AlertState.sendSupportMail()
}
return .none
case .sendSupportMailFinished:
state.supportData = nil
@ -117,6 +121,7 @@ struct SettingsReducer: ReducerProtocol {
return .none
}
}
.ifLet(\.$alert, action: /Action.alert)
Scope(state: \.phraseDisplayState, action: /Action.phraseDisplay) {
RecoveryPhraseDisplayReducer()
@ -164,6 +169,30 @@ extension SettingsStore {
}
}
// MARK: Alerts
extension AlertState where Action == SettingsReducer.Action {
static func cantBackupWallet(_ error: ZcashError) -> AlertState<SettingsReducer.Action> {
AlertState<SettingsReducer.Action> {
TextState(L10n.Settings.Alert.CantBackupWallet.title)
} message: {
TextState(L10n.Settings.Alert.CantBackupWallet.message(error.message, error.code.rawValue))
}
}
static func sendSupportMail() -> AlertState<SettingsReducer.Action> {
AlertState<SettingsReducer.Action> {
TextState(L10n.Settings.Alert.CantSendEmail.title)
} actions: {
ButtonState(action: .sendSupportMailFinished) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.Settings.Alert.CantSendEmail.message)
}
}
}
// MARK: Placeholders
extension SettingsReducer.State {

View File

@ -88,6 +88,14 @@ struct SettingsView: View {
.frame(width: 0, height: 0)
}
}
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
.alert(store: store.scope(
state: \.exportLogsState.$alert,
action: { .exportLogs(.alert($0)) }
))
}
@ViewBuilder func shareLogsView(_ viewStore: SettingsViewStore) -> some View {

View File

@ -3,6 +3,7 @@ import SwiftUI
import ZcashLightClientKit
import Utils
import Models
import Generated
typealias WalletEventsFlowStore = Store<WalletEventsFlowReducer.State, WalletEventsFlowReducer.Action>
typealias WalletEventsFlowViewStore = ViewStore<WalletEventsFlowReducer.State, WalletEventsFlowReducer.Action>
@ -17,8 +18,8 @@ struct WalletEventsFlowReducer: ReducerProtocol {
case showWalletEvent(WalletEvent)
}
@PresentationState var alert: AlertState<Action>?
var destination: Destination?
var latestMinedHeight: BlockHeight?
var isScrollable = true
var requiredTransactionConfirmations = 0
@ -27,8 +28,9 @@ struct WalletEventsFlowReducer: ReducerProtocol {
}
enum Action: Equatable {
case alert(AlertRequest)
case alert(PresentationAction<Action>)
case copyToPastboard(RedactableString)
case dismissAlert
case onAppear
case onDisappear
case openBlockExplorer(URL?)
@ -99,9 +101,13 @@ struct WalletEventsFlowReducer: ReducerProtocol {
case .alert:
return .none
case .dismissAlert:
return .none
case .warnBeforeLeavingApp(let blockExplorerURL):
return EffectTask(value: .alert(.walletEvents(.warnBeforeLeavingApp(blockExplorerURL))))
state.alert = AlertState.warnBeforeLeavingApp(blockExplorerURL)
return .none
case .openBlockExplorer(let blockExplorerURL):
if let url = blockExplorerURL {
@ -139,6 +145,25 @@ extension WalletEventsFlowViewStore {
}
}
// MARK: Alerts
extension AlertState where Action == WalletEventsFlowReducer.Action {
static func warnBeforeLeavingApp(_ blockExplorerURL: URL?) -> AlertState<WalletEventsFlowReducer.Action> {
AlertState<WalletEventsFlowReducer.Action> {
TextState(L10n.WalletEvent.Alert.LeavingApp.title)
} actions: {
ButtonState(action: .openBlockExplorer(blockExplorerURL)) {
TextState(L10n.WalletEvent.Alert.LeavingApp.Button.seeOnline)
}
ButtonState(role: .cancel, action: .dismissAlert) {
TextState(L10n.WalletEvent.Alert.LeavingApp.Button.nevermind)
}
} message: {
TextState(L10n.WalletEvent.Alert.LeavingApp.message)
}
}
}
// MARK: Placeholders
extension TransactionState {

View File

@ -6,7 +6,7 @@ struct WalletEventsFlowView: View {
let store: WalletEventsFlowStore
var body: some View {
return WithViewStore(store) { viewStore in
WithViewStore(store) { viewStore in
List {
walletEventsList(with: viewStore)
}
@ -18,6 +18,10 @@ struct WalletEventsFlowView: View {
viewStore.selectedWalletEvent?.detailView(store)
}
}
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
}
}

View File

@ -29,7 +29,7 @@ class BalanceBreakdownTests: XCTestCase {
// expected side effects as a result of .onAppear registration
store.receive(.synchronizerStateChanged(.zero))
store.receive(.updateLatestBlock) { state in
state.latestBlock = "nil"
state.latestBlock = ""
}
// long-living (cancelable) effects need to be properly canceled.
@ -54,9 +54,8 @@ class BalanceBreakdownTests: XCTestCase {
}
await store.receive(.shieldFundsSuccess) { state in
state.shieldingFunds = false
state.alert = AlertState.shieldFundsSuccess()
}
await store.receive(.alert(.balanceBreakdown(.shieldFundsSuccess)))
// long-living (cancelable) effects need to be properly canceled.
// the .onDisappear action cancels the observer of the synchronizer status change.
@ -80,16 +79,9 @@ class BalanceBreakdownTests: XCTestCase {
}
await store.receive(.shieldFundsFailure(ZcashError.synchronizerNotPrepared)) { state in
state.shieldingFunds = false
state.alert = AlertState.shieldFundsFailure(ZcashError.synchronizerNotPrepared)
}
await store.receive(
.alert(
.balanceBreakdown(
.shieldFundsFailure(ZcashError.synchronizerNotPrepared)
)
)
)
// long-living (cancelable) effects need to be properly canceled.
// the .onDisappear action cancels the observer of the synchronizer status change.
await store.send(.onDisappear)

View File

@ -9,6 +9,7 @@ import Combine
import XCTest
import ComposableArchitecture
import Utils
import Generated
@testable import secant_testnet
@testable import ZcashLightClientKit
@ -106,14 +107,8 @@ class HomeTests: XCTestCase {
state.synchronizerStatusSnapshot = errorSnapshot
}
store.receive(.showSynchronizerErrorAlert(testError))
store.receive(
.alert(
.home(
.syncFailed(ZcashError.synchronizerNotPrepared, "Dismiss")
)
)
)
store.receive(.showSynchronizerErrorAlert(testError)) { state in
state.alert = AlertState.syncFailed(ZcashError.synchronizerNotPrepared, L10n.Home.SyncFailed.dismiss)
}
}
}

View File

@ -287,9 +287,9 @@ class ImportWalletTests: XCTestCase {
store.dependencies.mnemonic = .noOp
store.dependencies.walletStorage = .noOp
store.send(.restoreWallet)
store.receive(.alert(.importWallet(.succeed)))
store.send(.restoreWallet) { state in
state.alert = AlertState.succeed()
}
store.receive(.initializeSDK)
}

View File

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

View File

@ -118,14 +118,7 @@ class RootTests: XCTestCase {
store.send(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
state.appInitializationState = .keysMissing
}
store.receive(.alert(.root(.walletStateFailed(.keysMissing)))) { state in
state.uniAlert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("App initialisation state: keysMissing."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
state.alert = AlertState.walletStateFailed(.keysMissing)
}
}
@ -150,24 +143,11 @@ class RootTests: XCTestCase {
store.receive(.initialization(.checkBackupPhraseValidation)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
state.alert = AlertState.cantLoadSeedPhrase()
}
store.receive(.initialization(.initializationFailed(zcashError)))
store.receive(.alert(.root(.cantLoadSeedPhrase))) { state in
state.uniAlert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
store.receive(.alert(.root(.initializationFailed(zcashError)))) { state in
state.uniAlert = AlertState(
title: TextState("Failed to initialize the SDK"),
message: TextState("Error: \(zcashError.message) (code: \(zcashError.code.rawValue))"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
store.receive(.initialization(.initializationFailed(zcashError))) { state in
state.alert = AlertState.initializationFailed(zcashError)
}
}
@ -190,24 +170,11 @@ class RootTests: XCTestCase {
store.receive(.initialization(.checkBackupPhraseValidation)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
state.alert = AlertState.cantLoadSeedPhrase()
}
store.receive(.initialization(.initializationFailed(zcashError)))
store.receive(.alert(.root(.cantLoadSeedPhrase))) { state in
state.uniAlert = AlertState(
title: TextState("Wallet initialisation failed."),
message: TextState("Can't load seed phrase from local storage."),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
}
store.receive(.alert(.root(.initializationFailed(zcashError)))) { state in
state.uniAlert = AlertState(
title: TextState("Failed to initialize the SDK"),
message: TextState("Error: \(zcashError.message) (code: \(zcashError.code.rawValue))"),
dismissButton: .default(TextState("Ok"), action: .send(.dismissAlert))
)
store.receive(.initialization(.initializationFailed(zcashError))) { state in
state.alert = AlertState.initializationFailed(zcashError)
}
}