[#557] Nav Changes (#602)

- previous profile screen connected to the receive ZEC button
- receive ZEC is now simplified to show only QR code + UA with small "i" icon leading to address details
- profile's UA address copy to pasteboard added
- home's settings button connected to settings screen
- settings screen updated, test crash report and rescan blockchain moved to debug menu
- root reducer's debug code move to a separate file
- unit tests updated + debug tests provided
This commit is contained in:
Lukas Korba 2023-03-02 15:24:32 +01:00 committed by GitHub
parent f1c9b06123
commit 49d858d22a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 384 additions and 381 deletions

View File

@ -447,6 +447,9 @@
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; };
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; };
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */; };
9E852D6129B098F400CF4AC1 /* RootDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D6029B098F400CF4AC1 /* RootDebug.swift */; };
9E852D6229B098F400CF4AC1 /* RootDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D6029B098F400CF4AC1 /* RootDebug.swift */; };
9E852D6529B0A86300CF4AC1 /* DebugTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D6429B0A86300CF4AC1 /* DebugTests.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 */; };
9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; };
@ -770,6 +773,8 @@
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>"; };
9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationFlowFeatureFlagTests.swift; sourceTree = "<group>"; };
9E852D6029B098F400CF4AC1 /* RootDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootDebug.swift; sourceTree = "<group>"; };
9E852D6429B0A86300CF4AC1 /* DebugTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTests.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>"; };
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
@ -1852,6 +1857,7 @@
children = (
9EAFEB812805793200199FC9 /* RootTests.swift */,
9E391131284644580073DD9A /* AppInitializationTests.swift */,
9E852D6429B0A86300CF4AC1 /* DebugTests.swift */,
);
path = RootTests;
sourceTree = "<group>";
@ -2104,6 +2110,7 @@
F9971A4C27680DC400A2DB75 /* RootView.swift */,
9E9ADA7E2938F5EC0071767B /* RootDestination.swift */,
9E9ADA7C2938F4C00071767B /* RootInitialization.swift */,
9E852D6029B098F400CF4AC1 /* RootDebug.swift */,
);
path = Root;
sourceTree = "<group>";
@ -2686,6 +2693,7 @@
0D26AF01299E8196005260EE /* RecoveryPhraseDisplayView.swift in Sources */,
0D26AF02299E8196005260EE /* URIParser.swift in Sources */,
0D26AF03299E8196005260EE /* URIParserLive.swift in Sources */,
9E852D6229B098F400CF4AC1 /* RootDebug.swift in Sources */,
34F682ED29A763FD0022C079 /* WalletConfigProvider.swift in Sources */,
0D26AF04299E8196005260EE /* LocalAuthenticationTestKey.swift in Sources */,
0D26AF05299E8196005260EE /* ScanView.swift in Sources */,
@ -2915,6 +2923,7 @@
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */,
9EB863A2292398A8003D0F8B /* URIParser.swift in Sources */,
9EB863C12923C779003D0F8B /* URIParserLive.swift in Sources */,
9E852D6129B098F400CF4AC1 /* RootDebug.swift in Sources */,
34F682EC29A763FD0022C079 /* WalletConfigProvider.swift in Sources */,
9EBDF987291F91EF000A1A05 /* LocalAuthenticationTestKey.swift in Sources */,
F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */,
@ -3053,6 +3062,7 @@
9EAFEB862805A23100199FC9 /* SecItemClientTests.swift in Sources */,
3448CB3728E485CB006ADEDB /* NotEnoughFeeSpaceSnapshots.swift in Sources */,
9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */,
9E852D6529B0A86300CF4AC1 /* DebugTests.swift in Sources */,
9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */,
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */,

View File

@ -4,9 +4,11 @@
//
// Created by Francisco Gindre on 2/2/23.
//
import ComposableArchitecture
import FirebaseCore
import FirebaseCrashlytics
extension CrashReporterClient: DependencyKey {
static let liveValue = CrashReporterClient(
configure: { canConfigure in

View File

@ -6,6 +6,7 @@
//
import ComposableArchitecture
extension CrashReporterClient: TestDependencyKey {
static let testValue = CrashReporterClient(
configure: { _ in },

View File

@ -4,6 +4,7 @@
//
// Created by Francisco Gindre on 2/2/23.
//
import ComposableArchitecture
import Foundation

View File

@ -16,8 +16,8 @@ struct HomeReducer: ReducerProtocol {
case balanceBreakdown
case notEnoughFreeDiskSpace
case profile
case request
case send
case settings
case transactionHistory
}
@ -25,10 +25,10 @@ struct HomeReducer: ReducerProtocol {
var balanceBreakdownState: BalanceBreakdownReducer.State
var destination: Destination?
var profileState: ProfileReducer.State
var requestState: RequestReducer.State
var requiredTransactionConfirmations = 0
var scanState: ScanReducer.State
var sendState: SendFlowReducer.State
var settingsState: SettingsReducer.State
var shieldedBalance: Balance
var synchronizerStatusSnapshot: SyncStatusSnapshot
var walletEventsState: WalletEventsFlowReducer.State
@ -61,9 +61,8 @@ struct HomeReducer: ReducerProtocol {
case onAppear
case onDisappear
case profile(ProfileReducer.Action)
case request(RequestReducer.Action)
case rewindDone(String?, SettingsReducer.Action)
case send(SendFlowReducer.Action)
case settings(SettingsReducer.Action)
case synchronizerStateChanged(SDKSynchronizerState)
case walletEvents(WalletEventsFlowReducer.Action)
case updateDestination(HomeReducer.State.Destination?)
@ -86,6 +85,10 @@ struct HomeReducer: ReducerProtocol {
SendFlowReducer()
}
Scope(state: \.settingsState, action: /Action.settings) {
SettingsReducer()
}
Scope(state: \.profileState, action: /Action.profile) {
ProfileReducer()
}
@ -137,45 +140,12 @@ struct HomeReducer: ReducerProtocol {
case .profile(.back):
state.destination = nil
return .none
case .profile(.settings(.quickRescan)):
state.destination = nil
return .run { send in
do {
try await sdkSynchronizer.rewind(.quick)
await send(.rewindDone(nil, .quickRescan))
} catch {
await send(.rewindDone(error.localizedDescription, .quickRescan))
}
}
case .profile(.settings(.fullRescan)):
state.destination = nil
return .run { send in
do {
try await sdkSynchronizer.rewind(.birthday)
await send(.rewindDone(nil, .fullRescan))
} catch {
await send(.rewindDone(error.localizedDescription, .fullRescan))
}
}
case .settings:
return .none
case .profile:
return .none
case .request:
return .none
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:
return .none
@ -221,13 +191,6 @@ extension HomeStore {
)
}
func requestStore() -> RequestStore {
self.scope(
state: \.requestState,
action: HomeReducer.Action.request
)
}
func sendStore() -> SendFlowStore {
self.scope(
state: \.sendState,
@ -235,6 +198,13 @@ extension HomeStore {
)
}
func settingsStore() -> SettingsStore {
self.scope(
state: \.settingsState,
action: HomeReducer.Action.settings
)
}
func balanceBreakdownStore() -> BalanceBreakdownStore {
self.scope(
state: \.balanceBreakdownState,
@ -263,9 +233,9 @@ extension HomeReducer.State {
.init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .default,
walletEventsState: .emptyPlaceHolder

View File

@ -8,9 +8,9 @@ struct HomeView: View {
WithViewStore(store) { viewStore in
VStack {
HStack {
profileButton(viewStore)
Spacer()
settingsButton(viewStore)
}
balance(viewStore)
@ -18,6 +18,8 @@ struct HomeView: View {
Spacer()
sendButton(viewStore)
receiveButton(viewStore)
Button {
viewStore.send(.updateDestination(.transactionHistory))
@ -48,21 +50,21 @@ struct HomeView: View {
// MARK: - Buttons
extension HomeView {
func profileButton(_ viewStore: HomeViewStore) -> some View {
func settingsButton(_ viewStore: HomeViewStore) -> some View {
Image(Asset.Assets.Icons.profile.name)
.resizable()
.frame(width: 60, height: 60)
.padding(.trailing, 15)
.navigationLink(
isActive: viewStore.bindingForDestination(.profile),
isActive: viewStore.bindingForDestination(.settings),
destination: {
ProfileView(store: store.profileStore())
SettingsView(store: store.settingsStore())
}
)
}
func sendButton(_ viewStore: HomeViewStore) -> some View {
Text("Send")
Text("Send ZEC")
.shadow(color: Asset.Colors.Buttons.buttonsTitleShadow.color, radius: 2, x: 0, y: 2)
.frame(
minWidth: 0,
@ -85,6 +87,30 @@ extension HomeView {
.padding(.bottom, 30)
}
func receiveButton(_ viewStore: HomeViewStore) -> some View {
Text("Receive ZEC")
.shadow(color: Asset.Colors.Buttons.buttonsTitleShadow.color, radius: 2, x: 0, y: 2)
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.foregroundColor(Asset.Colors.Text.activeButtonText.color)
.background(Asset.Colors.Buttons.activeButton.color)
.cornerRadius(12)
.frame(height: 60)
.padding(.horizontal, 50)
.neumorphicButton()
.navigationLink(
isActive: viewStore.bindingForDestination(.profile),
destination: {
ProfileView(store: store.profileStore())
}
)
.padding(.bottom, 30)
}
func balance(_ viewStore: HomeViewStore) -> some View {
Group {
Button {

View File

@ -9,7 +9,6 @@ struct ProfileReducer: ReducerProtocol {
struct State: Equatable {
enum Destination {
case addressDetails
case settings
}
var addressDetailsState: AddressDetailsReducer.State
@ -17,7 +16,6 @@ struct ProfileReducer: ReducerProtocol {
var appVersion = ""
var destination: Destination?
var sdkVersion = ""
var settingsState: SettingsReducer.State
var unifiedAddress: String {
addressDetailsState.uAddress?.stringEncoded ?? "could not extract UA"
@ -27,12 +25,13 @@ struct ProfileReducer: ReducerProtocol {
enum Action: Equatable {
case addressDetails(AddressDetailsReducer.Action)
case back
case copyUnifiedAddressToPastboard
case onAppear
case settings(SettingsReducer.Action)
case updateDestination(ProfileReducer.State.Destination?)
}
@Dependency(\.appVersion) var appVersion
@Dependency(\.pasteboard) var pasteboard
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@ -41,10 +40,6 @@ struct ProfileReducer: ReducerProtocol {
AddressDetailsReducer()
}
Scope(state: \.settingsState, action: /Action.settings) {
SettingsReducer()
}
Reduce { state, action in
switch action {
case .onAppear:
@ -56,6 +51,10 @@ struct ProfileReducer: ReducerProtocol {
case .back:
return .none
case .copyUnifiedAddressToPastboard:
pasteboard.setString(state.unifiedAddress.redacted)
return .none
case let .updateDestination(destination):
state.destination = destination
@ -63,9 +62,6 @@ struct ProfileReducer: ReducerProtocol {
case .addressDetails:
return .none
case .settings:
return .none
}
}
}
@ -80,13 +76,6 @@ extension ProfileStore {
action: ProfileReducer.Action.addressDetails
)
}
func settingsStore() -> SettingsStore {
self.scope(
state: \.settingsState,
action: ProfileReducer.Action.settings
)
}
}
// MARK: - ViewStore
@ -105,13 +94,6 @@ extension ProfileViewStore {
embed: { $0 ? .addressDetails : nil }
)
}
var bindingForSettings: Binding<Bool> {
self.destinationBinding.map(
extract: { $0 == .settings },
embed: { $0 ? .settings : nil }
)
}
}
// MARK: - Placeholders
@ -120,8 +102,7 @@ extension ProfileReducer.State {
static var placeholder: Self {
.init(
addressDetailsState: .placeholder,
destination: nil,
settingsState: .placeholder
destination: nil
)
}
}

View File

@ -6,62 +6,32 @@ struct ProfileView: View {
var body: some View {
WithViewStore(store) { viewStore in
ScrollView {
VStack {
qrCodeUA(viewStore.unifiedAddress)
.padding(.top, 30)
Text("Your UA address \(viewStore.unifiedAddress)")
.truncationMode(.middle)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(30)
.padding(.vertical, 50)
Button(
action: { viewStore.send(.updateDestination(.addressDetails)) },
label: { Text("See address details") }
)
.activeButtonStyle
.frame(height: 50)
.padding(EdgeInsets(top: 0, leading: 30, bottom: 50, trailing: 30))
Rectangle()
.frame(height: 1.5)
.padding(EdgeInsets(top: 0, leading: 100, bottom: 20, trailing: 100))
.foregroundColor(Asset.Colors.TextField.Underline.purple.color)
Button(
action: { viewStore.send(.updateDestination(.settings)) },
label: { Text("Settings") }
)
.primaryButtonStyle
.frame(height: 50)
.padding(EdgeInsets(top: 30, leading: 30, bottom: 20, trailing: 30))
Spacer()
HStack {
VStack {
Text("secant v\(viewStore.appVersion)(\(viewStore.appBuild))")
Text("sdk v\(viewStore.sdkVersion)")
}
Spacer()
Button(
action: { },
label: {
Text("More info")
.foregroundColor(Asset.Colors.Text.moreInfoText.color)
Text("Your UA")
.fontWeight(.bold)
.onTapGesture {
viewStore.send(.copyUnifiedAddressToPastboard)
}
)
Button {
viewStore.send(.updateDestination(.addressDetails))
} label: {
Image(systemName: "info.circle")
.offset(x: -10, y: -10)
.tint(.black)
}
}
.padding(30)
Text("\(viewStore.unifiedAddress)")
.padding(30)
Spacer()
}
.onAppear(perform: { viewStore.send(.onAppear) })
.navigationLinkEmpty(
isActive: viewStore.bindingForSettings,
destination: {
SettingsView(store: store.settingsStore())
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForAddressDetails,
destination: {
@ -104,14 +74,11 @@ struct ProfileView_Previews: PreviewProvider {
NavigationView {
ProfileView(
store: .init(
initialState: .init(
addressDetailsState: .placeholder,
settingsState: .placeholder
),
initialState: .init(addressDetailsState: .placeholder),
reducer: ProfileReducer()
)
)
}
.preferredColorScheme(.dark)
.preferredColorScheme(.light)
}
}

View File

@ -0,0 +1,117 @@
//
// RootDebug.swift
// secant
//
// Created by Lukáš Korba on 02.03.2023.
//
import Foundation
import ComposableArchitecture
import ZcashLightClientKit
/// In this file is a collection of helpers that control all state and action related operations
/// for the `RootReducer` with a connection to the UI navigation.
extension RootReducer {
struct DebugState: Equatable {
var rescanDialog: ConfirmationDialogState<RootReducer.Action>?
}
indirect enum DebugAction: Equatable {
case cancelRescan
case flagUpdated
case fullRescan
case quickRescan
case rescanBlockchain
case rewindDone(String?, RootReducer.Action)
case testCrashReporter // this will crash the app if live.
case updateFlag(FeatureFlag, Bool)
case walletConfigLoaded(WalletConfig)
}
// swiftlint:disable:next cyclomatic_complexity
func debugReduce() -> Reduce<RootReducer.State, RootReducer.Action> {
Reduce { state, action in
switch action {
case .debug(.testCrashReporter):
crashReporter.testCrash()
return .none
case .debug(.rescanBlockchain):
state.debugState.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.debug(.quickRescan))),
.default(TextState("Full rescan"), action: .send(.debug(.fullRescan))),
.cancel(TextState("Cancel"))
]
)
return .none
case .debug(.cancelRescan):
state.debugState.rescanDialog = nil
return .none
case .debug(.quickRescan):
state.destinationState.destination = .home
return .run { send in
do {
try await sdkSynchronizer.rewind(.quick)
await send(.debug(.rewindDone(nil, .debug(.quickRescan))))
} catch {
await send(.debug(.rewindDone(error.localizedDescription, .debug(.quickRescan))))
}
}
case .debug(.fullRescan):
state.destinationState.destination = .home
return .run { send in
do {
try await sdkSynchronizer.rewind(.birthday)
await send(.debug(.rewindDone(nil, .debug(.fullRescan))))
} catch {
await send(.debug(.rewindDone(error.localizedDescription, .debug(.fullRescan))))
}
}
case let .debug(.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 let .debug(.updateFlag(flag, isEnabled)):
return walletConfigProvider.update(flag, !isEnabled)
.receive(on: mainQueue)
.map { _ in return Action.debug(.flagUpdated) }
.eraseToEffect()
.cancellable(id: WalletConfigCancelId.self, cancelInFlight: true)
case .debug(.flagUpdated):
return walletConfigProvider.load()
.receive(on: mainQueue)
.map { Action.debug(.walletConfigLoaded($0)) }
.eraseToEffect()
.cancellable(id: WalletConfigCancelId.self, cancelInFlight: true)
case let .debug(.walletConfigLoaded(walletConfig)):
return EffectTask(value: .updateStateAfterConfigUpdate(walletConfig))
default: return .none
}
}
}
}
// MARK: Placeholders
extension RootReducer.DebugState {
static var placeholder: Self {
.init()
}
}

View File

@ -134,8 +134,8 @@ extension RootReducer {
state.destinationState.alert = nil
return .none
case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation,
.sandbox, .welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .dismissAlert:
case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .updateStateAfterConfigUpdate,
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .dismissAlert:
return .none
}

View File

@ -26,12 +26,6 @@ extension RootReducer {
case walletConfigChanged(WalletConfig)
}
enum DebugAction: Equatable {
case updateFlag(FeatureFlag, Bool)
case flagUpdated
case walletConfigLoaded(WalletConfig)
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
func initializationReduce() -> Reduce<RootReducer.State, RootReducer.Action> {
Reduce { state, action in
@ -56,8 +50,10 @@ extension RootReducer {
}
case .initialization(.walletConfigChanged(let walletConfig)):
updateStateAfterConfigUpdate(state: &state, config: walletConfig)
return EffectTask(value: .initialization(.initialSetups))
return .concatenate(
EffectTask(value: .updateStateAfterConfigUpdate(walletConfig)),
EffectTask(value: .initialization(.initialSetups))
)
case .initialization(.initialSetups):
// TODO: [#524] finish all the wallet events according to definition, https://github.com/zcash/secant-ios-wallet/issues/524
@ -307,43 +303,26 @@ extension RootReducer {
case .onboarding(.createNewWallet):
return EffectTask(value: .initialization(.createNewWallet))
case .home, .destination, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .welcome, .binding:
return .none
case .initialization(.configureCrashReporter):
crashReporter.configure(
!userStoredPreferences.isUserOptedOutOfCrashReporting()
)
return .none
case .updateStateAfterConfigUpdate(let walletConfig):
state.walletConfig = walletConfig
state.onboardingState.walletConfig = walletConfig
return .none
case .dismissAlert:
state.alert = nil
return .none
case let .debug(.updateFlag(flag, isEnabled)):
return walletConfigProvider.update(flag, !isEnabled)
.receive(on: mainQueue)
.map { _ in return Action.debug(.flagUpdated) }
.eraseToEffect()
.cancellable(id: WalletConfigCancelId.self, cancelInFlight: true)
case .debug(.flagUpdated):
return walletConfigProvider.load()
.receive(on: mainQueue)
.map { Action.debug(.walletConfigLoaded($0)) }
.eraseToEffect()
.cancellable(id: WalletConfigCancelId.self, cancelInFlight: true)
case let .debug(.walletConfigLoaded(walletConfig)):
updateStateAfterConfigUpdate(state: &state, config: walletConfig)
case .home, .destination, .onboarding, .phraseDisplay, .phraseValidation, .sandbox,
.welcome, .binding, .debug:
return .none
}
}
}
private func updateStateAfterConfigUpdate(state: inout RootReducer.State, config: WalletConfig) {
state.walletConfig = config
state.onboardingState.walletConfig = config
}
}

View File

@ -12,6 +12,7 @@ struct RootReducer: ReducerProtocol {
struct State: Equatable {
@BindingState var alert: AlertState<RootReducer.Action>?
var appInitializationState: InitializationState = .uninitialized
var debugState: DebugState
var destinationState: DestinationState
var homeState: HomeReducer.State
var onboardingState: OnboardingFlowReducer.State
@ -36,6 +37,7 @@ struct RootReducer: ReducerProtocol {
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case phraseValidation(RecoveryPhraseValidationFlowReducer.Action)
case sandbox(SandboxReducer.Action)
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(WelcomeReducer.Action)
}
@ -83,6 +85,8 @@ struct RootReducer: ReducerProtocol {
initializationReduce()
destinationReduce()
debugReduce()
}
}
@ -172,6 +176,7 @@ extension RootReducer {
extension RootReducer.State {
static var placeholder: Self {
.init(
debugState: .placeholder,
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(

View File

@ -126,6 +126,14 @@ private extension RootView {
viewStore.goToDestination(.welcome)
}
Button("Test Crash Reporter") {
viewStore.send(.debug(.testCrashReporter))
}
Button("Rescan Blockchain") {
viewStore.send(.debug(.rescanBlockchain))
}
Button("[Be careful] Nuke Wallet") {
viewStore.send(.initialization(.nukeWalletRequest))
}
@ -156,6 +164,10 @@ private extension RootView {
}
}
.alert(self.store.scope(state: \.destinationState.alert), dismiss: .destination(.dismissAlert))
.confirmationDialog(
store.scope(state: \.debugState.rescanDialog),
dismiss: .debug(.cancelRescan)
)
}
.navigationBarTitle("Startup")
}

View File

@ -17,7 +17,6 @@ struct SettingsReducer: ReducerProtocol {
@BindingState var isCrashReportingOn: Bool
var isSharingLogs = false
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
var supportData: SupportData?
var tempSDKDir: URL {
@ -43,20 +42,15 @@ struct SettingsReducer: ReducerProtocol {
case backupWallet
case backupWalletAccessRequest
case binding(BindingAction<SettingsReducer.State>)
case cancelRescan
case dismissAlert
case exportLogs
case fullRescan
case logsExported
case logsExportFailed(String)
case logsShareFinished
case onAppear
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan
case rescanBlockchain
case sendSupportMail
case sendSupportMailFinished
case testCrashReporter // this will crash the app if live.
case updateDestination(SettingsReducer.State.Destination?)
}
@ -108,10 +102,6 @@ struct SettingsReducer: ReducerProtocol {
return .run { [state] _ in
await userStoredPreferences.setIsUserOptedOutOfCrashReporting(state.isCrashReportingOn)
}
case .cancelRescan, .quickRescan, .fullRescan:
state.rescanDialog = nil
return .none
case .dismissAlert:
state.alert = nil
@ -146,18 +136,6 @@ struct SettingsReducer: ReducerProtocol {
state.isSharingLogs = false
return .none
case .rescanBlockchain:
state.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
return .none
case .phraseDisplay:
state.destination = nil
return .none
@ -166,10 +144,6 @@ struct SettingsReducer: ReducerProtocol {
state.destination = destination
return .none
case .testCrashReporter:
crashReporter.testCrash()
return .none
case .binding:
return .none

View File

@ -18,13 +18,6 @@ struct SettingsView: View {
.activeButtonStyle
.frame(height: 50)
Button(
action: { viewStore.send(.rescanBlockchain) },
label: { Text("Rescan Blockchain") }
)
.primaryButtonStyle
.frame(height: 50)
Button(
action: { viewStore.send(.exportLogs) },
label: {
@ -42,13 +35,6 @@ struct SettingsView: View {
.frame(height: 50)
.disabled(viewStore.exportLogsDisabled)
Button(
action: { viewStore.send(.testCrashReporter) },
label: { Text("Test Crash Reporter") }
)
.primaryButtonStyle
.frame(height: 50)
Button(
action: { viewStore.send(.sendSupportMail) },
label: { Text("Send us feedback!") }
@ -61,10 +47,6 @@ struct SettingsView: View {
.padding(.horizontal, 30)
.navigationTitle("Settings")
.applyScreenBackground()
.confirmationDialog(
store.scope(state: \.rescanDialog),
dismiss: .cancelRescan
)
.navigationLinkEmpty(
isActive: viewStore.bindingForBackupPhrase,
destination: {

View File

@ -87,54 +87,4 @@ class HomeTests: XCTestCase {
// the .onDisappear action cancles the observer of the synchronizer status change.
store.send(.onDisappear)
}
@MainActor func testQuickRescan_ResetToHomeScreen() async throws {
let homeState = HomeReducer.State(
balanceBreakdownState: .placeholder,
destination: .profile,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .default,
walletEventsState: .emptyPlaceHolder
)
let store = TestStore(
initialState: homeState,
reducer: HomeReducer()
)
await store.send(.profile(.settings(.quickRescan))) { state in
state.destination = nil
}
await store.receive(.rewindDone(nil, .quickRescan))
}
@MainActor func testFullRescan_ResetToHomeScreen() async throws {
let homeState = HomeReducer.State(
balanceBreakdownState: .placeholder,
destination: .profile,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .default,
walletEventsState: .emptyPlaceHolder
)
let store = TestStore(
initialState: homeState,
reducer: HomeReducer()
)
await store.send(.profile(.settings(.fullRescan))) { state in
state.destination = nil
}
await store.receive(.rewindDone(nil, .fullRescan))
}
}

View File

@ -11,6 +11,9 @@ import ComposableArchitecture
import ZcashLightClientKit
class ProfileTests: XCTestCase {
// swiftlint:disable line_length
let uAddressEncoding = "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h"
@MainActor func testSynchronizerStateChanged_AnyButSynced() async throws {
let store = TestStore(
initialState: .placeholder,
@ -20,9 +23,8 @@ class ProfileTests: XCTestCase {
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
}
// swiftlint:disable line_length
let uAddress = try UnifiedAddress(
encoding: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h",
encoding: uAddressEncoding,
network: .testnet
)
@ -33,4 +35,26 @@ class ProfileTests: XCTestCase {
state.sdkVersion = "0.18.1-beta"
}
}
func testCopyUnifiedAddressToPasteboard() throws {
let testPasteboard = PasteboardClient.testPasteboard
let uAddress = try UnifiedAddress(encoding: uAddressEncoding, network: .testnet)
let store = TestStore(
initialState: ProfileReducer.State(
addressDetailsState: AddressDetailsReducer.State(uAddress: uAddress)
),
reducer: ProfileReducer()
) {
$0.pasteboard = testPasteboard
}
store.send(.copyUnifiedAddressToPastboard)
XCTAssertEqual(
testPasteboard.getString()?.data,
uAddress.stringEncoded,
"AddressDetails: `testCopyUnifiedAddressToPasteboard` is expected to match the input `\(uAddress.stringEncoded)`"
)
}
}

View File

@ -161,6 +161,7 @@ class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase {
)
let appState = RootReducer.State(
debugState: .placeholder,
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(

View File

@ -68,6 +68,7 @@ class AppInitializationTests: XCTestCase {
let walletConfig = WalletConfig(flags: defaultRawFlags)
let appState = RootReducer.State(
debugState: .placeholder,
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(

View File

@ -0,0 +1,107 @@
//
// DebugTests.swift
// secantTests
//
// Created by Lukáš Korba on 02.03.2023.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
@MainActor
class DebugTests: XCTestCase {
func testRescanBlockchain() async throws {
let store = TestStore(
initialState: .placeholder,
reducer: RootReducer()
)
await store.send(.debug(.rescanBlockchain)) { state in
state.debugState.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.debug(.quickRescan))),
.default(TextState("Full rescan"), action: .send(.debug(.fullRescan))),
.cancel(TextState("Cancel"))
]
)
}
}
func testRescanBlockchain_Cancelling() async throws {
var mockState = RootReducer.State.placeholder
mockState.debugState.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.debug(.quickRescan))),
.default(TextState("Full rescan"), action: .send(.debug(.fullRescan))),
.cancel(TextState("Cancel"))
]
)
let store = TestStore(
initialState: mockState,
reducer: RootReducer()
)
await store.send(.debug(.cancelRescan)) { state in
state.debugState.rescanDialog = nil
}
}
func testRescanBlockchain_QuickRescanClearance() async throws {
var mockState = RootReducer.State.placeholder
mockState.debugState.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.debug(.quickRescan))),
.default(TextState("Full rescan"), action: .send(.debug(.fullRescan))),
.cancel(TextState("Cancel"))
]
)
let store = TestStore(
initialState: mockState,
reducer: RootReducer()
)
await store.send(.debug(.quickRescan)) { state in
state.destinationState.internalDestination = .home
state.destinationState.previousDestination = .welcome
}
await store.receive(.debug(.rewindDone(nil, .debug(.quickRescan))))
}
func testRescanBlockchain_FullRescanClearance() async throws {
var mockState = RootReducer.State.placeholder
mockState.debugState.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.debug(.quickRescan))),
.default(TextState("Full rescan"), action: .send(.debug(.fullRescan))),
.cancel(TextState("Cancel"))
]
)
let store = TestStore(
initialState: mockState,
reducer: RootReducer()
)
await store.send(.debug(.fullRescan)) { state in
state.destinationState.internalDestination = .home
state.destinationState.previousDestination = .welcome
}
await store.receive(.debug(.rewindDone(nil, .debug(.fullRescan))))
}
}

View File

@ -80,112 +80,12 @@ class SettingsTests: XCTestCase {
await store.finish()
}
func testRescanBlockchain() async throws {
let store = TestStore(
initialState: .placeholder,
reducer: SettingsReducer()
)
await store.send(.rescanBlockchain) { state in
state.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
}
}
func testRescanBlockchain_Cancelling() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
await store.send(.cancelRescan) { state in
state.rescanDialog = nil
}
}
func testRescanBlockchain_QuickRescanClearance() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
await store.send(.quickRescan) { state in
state.rescanDialog = nil
}
}
func testRescanBlockchain_FullRescanClearance() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
await store.send(.fullRescan) { state in
state.rescanDialog = nil
}
}
func testExportLogs_ButtonDisableShareEnable() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isCrashReportingOn: false,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
phraseDisplayState: .init()
),
reducer: SettingsReducer()
)
@ -208,16 +108,7 @@ class SettingsTests: XCTestCase {
destination: nil,
isCrashReportingOn: false,
isSharingLogs: true,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
phraseDisplayState: .init()
),
reducer: SettingsReducer()
)

View File

@ -39,9 +39,9 @@ class HomeSnapshotTests: XCTestCase {
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents))

View File

@ -54,9 +54,9 @@ class WalletEventsSnapshotTests: XCTestCase {
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent]))
@ -106,9 +106,9 @@ class WalletEventsSnapshotTests: XCTestCase {
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent]))
@ -158,9 +158,9 @@ class WalletEventsSnapshotTests: XCTestCase {
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent]))
@ -216,9 +216,9 @@ class WalletEventsSnapshotTests: XCTestCase {
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent]))

View File

@ -127,8 +127,10 @@ class WalletConfigProviderTests: XCTestCase {
defaultRawFlags[.onboardingFlow] = true
let flags = WalletConfig(flags: defaultRawFlags)
store.send(.debug(.walletConfigLoaded(flags)))
// The new flag's value has to be propagated to all `walletConfig` instances
store.send(.debug(.walletConfigLoaded(flags))) { state in
store.receive(.updateStateAfterConfigUpdate(flags)) { state in
state.walletConfig = flags
state.onboardingState.walletConfig = flags
}