From 6a12e09ee9650a32a457f05b0b7de679b6b785ea Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Wed, 4 May 2022 22:01:48 +0200 Subject: [PATCH] [#302] Synchronizer status on Home Screen (#304) * synchronizer status * amount input field enhancements Closes #302 --- secant.xcodeproj/project.pbxproj | 4 + secant/Features/Home/HomeStore.swift | 56 ++++++++- secant/Features/Home/Views/HomeView.swift | 5 +- .../Features/Sandbox/Views/SandboxView.swift | 1 - secant/Features/Send/SendStore.swift | 78 ++++++++++++- .../Send/Views/CreateTransactionView.swift | 107 +++++++++++------- secant/Features/Send/Views/SendView.swift | 12 +- .../Views/TransactionConfirmationView.swift | 2 +- .../Components/TextFieldStore.swift | 18 ++- .../TextFields/TransactionInputStore.swift | 40 ++++--- .../TextFields/TransactionTextField.swift | 38 ++++++- .../Wrappers/WrappedNotificationCenter.swift | 22 ++++ secant/Wrappers/WrappedSDKSynchronizer.swift | 72 ++++++++++-- .../UserPreferencesStorageTests.swift | 4 +- 14 files changed, 365 insertions(+), 94 deletions(-) create mode 100644 secant/Wrappers/WrappedNotificationCenter.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 12234e4..04d28a9 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -106,6 +106,7 @@ 9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */; }; 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF643281FEC9900BA3F17 /* SendTests.swift */; }; 9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */; }; + 9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */; }; 9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; }; 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; }; @@ -274,6 +275,7 @@ 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFailedView.swift; sourceTree = ""; }; 9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = ""; }; 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = ""; }; + 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = ""; }; 9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = ""; }; 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = ""; }; @@ -762,6 +764,7 @@ 9E02B5C2280458D2005B809B /* WrappedDerivationTool.swift */, 9EAFEB872806E5AE00199FC9 /* WrappedSDKSynchronizer.swift */, 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */, + 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */, ); path = Wrappers; sourceTree = ""; @@ -1232,6 +1235,7 @@ 0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */, 2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */, 9EAFEB8A2806F48100199FC9 /* ZCashSDKEnvironment.swift in Sources */, + 9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */, 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */, 9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */, 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */, diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index e23c927..f4b487a 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import SwiftUI +import ZcashLightClientKit struct HomeState: Equatable { enum Route: Equatable { @@ -16,6 +17,7 @@ struct HomeState: Equatable { var requestState: RequestState var sendState: SendState var scanState: ScanState + var synchronizerStatus: String var totalBalance: Double var transactionHistoryState: TransactionHistoryState var verifiedBalance: Double @@ -34,6 +36,7 @@ enum HomeAction: Equatable { case updateBalance(Balance) case updateDrawer(DrawerOverlay) case updateRoute(HomeState.Route?) + case updateSynchronizerStatus case updateTransactions([TransactionState]) } @@ -83,20 +86,19 @@ extension HomeReducer { .receive(on: environment.scheduler) .map({ Balance(verified: $0.verified, total: $0.total) }) .map(HomeAction.updateBalance) - .eraseToEffect() + .eraseToEffect(), + + Effect(value: .updateSynchronizerStatus) ) case .synchronizerStateChanged(let synchronizerState): - return .none + return Effect(value: .updateSynchronizerStatus) case .updateBalance(let balance): state.totalBalance = balance.total.asHumanReadableZecBalance() state.verifiedBalance = balance.verified.asHumanReadableZecBalance() return .none - case .debugMenuStartup: - return .none - case .updateDrawer(let drawerOverlay): state.drawerOverlay = drawerOverlay state.transactionHistoryState.isScrollable = drawerOverlay == .full ? true : false @@ -105,6 +107,10 @@ extension HomeReducer { case .updateTransactions(let transactions): return .none + case .updateSynchronizerStatus: + state.synchronizerStatus = environment.wrappedSDKSynchronizer.status() + return .none + case .updateRoute(let route): state.route = route return .none @@ -132,6 +138,9 @@ extension HomeReducer { case .send(let action): return .none + + case .debugMenuStartup: + return .none } } @@ -234,9 +243,46 @@ extension HomeState { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, + synchronizerStatus: "", totalBalance: 0.0, transactionHistoryState: .emptyPlaceHolder, verifiedBalance: 0.0 ) } } + +extension SDKSynchronizer { + static func textFor(state: SyncStatus) -> String { + switch state { + case .downloading(let progress): + return "Downloading \(progress.progressHeight)/\(progress.targetHeight)" + + case .enhancing(let enhanceProgress): + return "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)" + + case .fetching: + return "fetching UTXOs" + + case .scanning(let scanProgress): + return "Scanning: \(scanProgress.progressHeight)/\(scanProgress.targetHeight)" + + case .disconnected: + return "disconnected ๐Ÿ’”" + + case .stopped: + return "Stopped ๐Ÿšซ" + + case .synced: + return "Synced ๐Ÿ˜Ž" + + case .unprepared: + return "Unprepared ๐Ÿ˜…" + + case .validating: + return "Validating" + + case .error(let err): + return "Error: \(err.localizedDescription)" + } + } +} diff --git a/secant/Features/Home/Views/HomeView.swift b/secant/Features/Home/Views/HomeView.swift index 3a466b5..6b14546 100644 --- a/secant/Features/Home/Views/HomeView.swift +++ b/secant/Features/Home/Views/HomeView.swift @@ -15,11 +15,14 @@ struct HomeView: View { sendButton(viewStore) VStack { + Text("\(viewStore.synchronizerStatus)") + .padding(.top, 60) + Text("balance: \(viewStore.totalBalance)") .accessDebugMenuWithHiddenGesture { viewStore.send(.debugMenuStartup) } - .padding(.top, 180) + .padding(.top, 120) Spacer() } diff --git a/secant/Features/Sandbox/Views/SandboxView.swift b/secant/Features/Sandbox/Views/SandboxView.swift index 1cbb2be..f5a1041 100644 --- a/secant/Features/Sandbox/Views/SandboxView.swift +++ b/secant/Features/Sandbox/Views/SandboxView.swift @@ -19,7 +19,6 @@ struct SandboxView: View { case .history: TransactionHistoryView(store: store.historyStore()) case .send: - EmptyView() SendView( store: .init( initialState: .placeholder, diff --git a/secant/Features/Send/SendStore.swift b/secant/Features/Send/SendStore.swift index 992d031..1b5a81c 100644 --- a/secant/Features/Send/SendStore.swift +++ b/secant/Features/Send/SendStore.swift @@ -35,12 +35,19 @@ struct SendState: Equatable { var route: Route? var isSendingTransaction = false + var totalBalance = 0.0 var transaction: Transaction + var transactionInputState: TransactionInputState } enum SendAction: Equatable { + case onAppear + case onDisappear case sendConfirmationPressed case sendTransactionResult(Result) + case synchronizerStateChanged(WrappedSDKSynchronizerState) + case transactionInput(TransactionInputAction) + case updateBalance(Double) case updateTransaction(Transaction) case updateRoute(SendState.Route?) } @@ -55,12 +62,23 @@ struct SendEnvironment { // MARK: - SendReducer +private struct ListenerId: Hashable {} + typealias SendReducer = Reducer extension SendReducer { private struct SyncStatusUpdatesID: Hashable {} - static let `default` = Reducer { state, action, environment in + static let `default` = SendReducer.combine( + [ + balanceReducer, + sendReducer, + transactionInputReducer + ] + ) + .debug() + + private static let sendReducer = SendReducer { state, action, environment in switch action { case let .updateTransaction(transaction): state.transaction = transaction @@ -111,9 +129,51 @@ extension SendReducer { } catch { return Effect(value: .updateRoute(.failure)) } + case .transactionInput(let transactionInput): + return .none + + default: + return .none } } + private static let balanceReducer = SendReducer { state, action, environment in + switch action { + case .onAppear: + return environment.wrappedSDKSynchronizer.stateChanged + .map(SendAction.synchronizerStateChanged) + .eraseToEffect() + .cancellable(id: ListenerId(), cancelInFlight: true) + + case .onDisappear: + return Effect.cancel(id: ListenerId()) + + case .synchronizerStateChanged(.synced): + return environment.wrappedSDKSynchronizer.getShieldedBalance() + .receive(on: environment.scheduler) + .map({ Double($0.total) / Double(100_000_000) }) + .map(SendAction.updateBalance) + .eraseToEffect() + + case .synchronizerStateChanged(let synchronizerState): + return .none + + case .updateBalance(let balance): + state.totalBalance = balance + state.transactionInputState.maxValue = Int64(balance * 100_000_000) + return .none + + default: + return .none + } + } + + private static let transactionInputReducer: SendReducer = TransactionInputReducer.default.pullback( + state: \SendState.transactionInputState, + action: /SendAction.transactionInput, + environment: { _ in TransactionInputEnvironment() } + ) + static func `default`(whenDone: @escaping () -> Void) -> SendReducer { SendReducer { state, action, environment in switch action { @@ -176,13 +236,24 @@ extension SendViewStore { embed: { $0 ? SendState.Route.done : SendState.Route.showConfirmation } ) } + + var bindingForBalance: Binding { + self.binding( + get: \.totalBalance, + send: SendAction.updateBalance + ) + } } // MARK: PlaceHolders extension SendState { static var placeholder: Self { - .init(route: nil, transaction: .placeholder) + .init( + route: nil, + transaction: .placeholder, + transactionInputState: .placeholer + ) } static var emptyPlaceholder: Self { @@ -192,7 +263,8 @@ extension SendState { amount: 0, memo: "", toAddress: "" - ) + ), + transactionInputState: .placeholer ) } } diff --git a/secant/Features/Send/Views/CreateTransactionView.swift b/secant/Features/Send/Views/CreateTransactionView.swift index 04b7b8f..6817f56 100644 --- a/secant/Features/Send/Views/CreateTransactionView.swift +++ b/secant/Features/Send/Views/CreateTransactionView.swift @@ -2,61 +2,76 @@ import SwiftUI import ComposableArchitecture struct CreateTransaction: View { + let store: TransactionInputStore + @Binding var transaction: Transaction @Binding var isComplete: Bool + @Binding var totalBalance: Double var body: some View { UITextView.appearance().backgroundColor = .clear - return VStack { + return WithViewStore(store) { viewStore in VStack { - Text("ZEC Amount") - - TextField( - "ZEC Amount", - text: $transaction.amountString - ) + VStack { + Text("Balance \(totalBalance)") + + SingleLineTextField( + placeholderText: "0", + title: "How much ZEC would you like to send?", + store: store.scope( + state: \.textFieldState, + action: TransactionInputAction.textField + ), + titleAccessoryView: { + Button( + action: { viewStore.send(.setMax(viewStore.maxValue)) }, + label: { Text("Max") } + ) + .textFieldTitleAccessoryButtonStyle + }, + inputAccessoryView: { + } + ) + } .padding() - .background(Color.white) - .foregroundColor(Asset.Colors.Text.importSeedEditor.color) - } - .padding() - - VStack { - Text("To Address") - TextField( - "Address", - text: $transaction.toAddress - ) - .font(.system(size: 14)) + VStack { + Text("To Address") + + TextField( + "Address", + text: $transaction.toAddress + ) + .font(.system(size: 14)) + .padding() + .background(Color.white) + .foregroundColor(Asset.Colors.Text.importSeedEditor.color) + } .padding() - .background(Color.white) - .foregroundColor(Asset.Colors.Text.importSeedEditor.color) - } - .padding() - - VStack { - Text("Memo") - TextEditor(text: $transaction.memo) - .frame(maxWidth: .infinity, maxHeight: 150, alignment: .center) - .importSeedEditorModifier() + VStack { + Text("Memo") + + TextEditor(text: $transaction.memo) + .frame(maxWidth: .infinity, maxHeight: 150, alignment: .center) + .importSeedEditorModifier() + } + .padding() + + Button( + action: { isComplete = true }, + label: { Text("Send") } + ) + .activeButtonStyle + .frame(height: 50) + .padding() + + Spacer() } .padding() - - Button( - action: { isComplete = true }, - label: { Text("Send") } - ) - .activeButtonStyle - .frame(height: 50) - .padding() - - Spacer() + .applyScreenBackground() } - .padding() - .applyScreenBackground() } } @@ -68,12 +83,15 @@ struct Create_Previews: PreviewProvider { StateContainer( initialState: ( Transaction.placeholder, - false + false, + 0.0 ) ) { CreateTransaction( + store: .placeholder, transaction: $0.0, - isComplete: $0.1 + isComplete: $0.1, + totalBalance: $0.2 ) } .navigationBarTitleDisplayMode(.inline) @@ -88,7 +106,8 @@ extension SendStore { return SendStore( initialState: .init( route: nil, - transaction: .placeholder + transaction: .placeholder, + transactionInputState: .placeholer ), reducer: .default, environment: SendEnvironment( diff --git a/secant/Features/Send/Views/SendView.swift b/secant/Features/Send/Views/SendView.swift index 885bd31..16bc459 100644 --- a/secant/Features/Send/Views/SendView.swift +++ b/secant/Features/Send/Views/SendView.swift @@ -7,9 +7,16 @@ struct SendView: View { var body: some View { WithViewStore(store) { viewStore in CreateTransaction( + store: store.scope( + state: \.transactionInputState, + action: SendAction.transactionInput + ), transaction: viewStore.bindingForTransaction, - isComplete: viewStore.bindingForConfirmation + isComplete: viewStore.bindingForConfirmation, + totalBalance: viewStore.bindingForBalance ) + .onAppear { viewStore.send(.onAppear) } + .onDisappear { viewStore.send(.onDisappear) } .navigationLinkEmpty( isActive: viewStore.bindingForConfirmation, destination: { @@ -35,7 +42,8 @@ struct SendView_Previews: PreviewProvider { store: .init( initialState: .init( route: nil, - transaction: .placeholder + transaction: .placeholder, + transactionInputState: .placeholer ), reducer: .default, environment: SendEnvironment( diff --git a/secant/Features/Send/Views/TransactionConfirmationView.swift b/secant/Features/Send/Views/TransactionConfirmationView.swift index 1c0577f..6de8142 100644 --- a/secant/Features/Send/Views/TransactionConfirmationView.swift +++ b/secant/Features/Send/Views/TransactionConfirmationView.swift @@ -6,7 +6,7 @@ struct TransactionConfirmation: View { var body: some View { VStack { - Text("Send \(String(format: "%.7f", Int64(viewStore.transaction.amount).asHumanReadableZecBalance())) ZEC") + Text("Send \(String(format: "%.7f", Int64(viewStore.transactionInputState.amount).asHumanReadableZecBalance())) ZEC") .padding() Text("To \(viewStore.transaction.toAddress)") diff --git a/secant/UIComponents/TextFields/Components/TextFieldStore.swift b/secant/UIComponents/TextFields/Components/TextFieldStore.swift index 00e61f6..8e4e4a8 100644 --- a/secant/UIComponents/TextFields/Components/TextFieldStore.swift +++ b/secant/UIComponents/TextFields/Components/TextFieldStore.swift @@ -21,8 +21,8 @@ struct TextFieldState: Equatable { } } -enum TextFieldAction { - case apply((String) -> String) +enum TextFieldAction: Equatable { +// case apply((String) -> String) case set(String) } @@ -31,10 +31,9 @@ struct TextFieldEnvironment: Equatable { } extension TextFieldReducer { static let `default` = TextFieldReducer { state, action, _ in switch action { - case .apply(let action): - state.text = action(state.text) - state.valid = state.text.isValid(for: state.validationType) - +// case .apply(let action): +// state.text = action(state.text) +// state.valid = state.text.isValid(for: state.validationType) case .set(let text): state.text = text state.valid = state.text.isValid(for: state.validationType) @@ -60,3 +59,10 @@ extension TextFieldStore { ) } } + +extension TextFieldState { + static let placeholder = TextFieldState( + validationType: nil, + text: "" + ) +} diff --git a/secant/UIComponents/TextFields/TransactionInputStore.swift b/secant/UIComponents/TextFields/TransactionInputStore.swift index 30f64d9..148ff4e 100644 --- a/secant/UIComponents/TextFields/TransactionInputStore.swift +++ b/secant/UIComponents/TextFields/TransactionInputStore.swift @@ -18,30 +18,21 @@ typealias TransactionInputStore = Store TransactionReducerData { - return { state, action in - switch action { - case .setMax(let value): - state.textFieldState.text = "\(value)" - state.currencySelectionState.currencyType = .usd - - default: break - } - - reducer(&state, action) - } -} - extension TransactionInputReducer { static let `default` = TransactionInputReducer.combine( [ @@ -56,7 +47,7 @@ extension TransactionInputReducer { switch action { case .setMax(let value): state.currencySelectionState.currencyType = .zec - state.textFieldState.text = "\(value)" + state.textFieldState.text = "\(value.asHumanReadableZecBalance())" default: break } @@ -99,3 +90,18 @@ extension TransactionInputReducer { environment: { _ in return .init() } ) } + +extension TransactionInputState { + static let placeholer = TransactionInputState( + textFieldState: .placeholder, + currencySelectionState: CurrencySelectionState() + ) +} + +extension TransactionInputStore { + static let placeholder = TransactionInputStore( + initialState: .placeholer, + reducer: .default, + environment: TransactionInputEnvironment() + ) +} diff --git a/secant/UIComponents/TextFields/TransactionTextField.swift b/secant/UIComponents/TextFields/TransactionTextField.swift index 0497357..4b200c4 100644 --- a/secant/UIComponents/TextFields/TransactionTextField.swift +++ b/secant/UIComponents/TextFields/TransactionTextField.swift @@ -14,7 +14,7 @@ struct TransactionTextField: View { // Constant example used here, this could be injected by a dependency // Access to this value could also be injected into the store as a dependency // with a function to prouce this value. - let maxTransactionValue = 500.0 + let maxTransactionValue: Int64 = 500 var body: some View { WithViewStore(store) { viewStore in @@ -68,5 +68,41 @@ struct TransactionTextField_Previews: PreviewProvider { .padding(.horizontal, 50) .applyScreenBackground() .previewLayout(.fixed(width: 500, height: 200)) + + SingleLineTextField( + placeholderText: "$0", + title: "How much?", + store: .transaction, + titleAccessoryView: { + Button( + action: { }, + label: { Text("Max") } + ) + .textFieldTitleAccessoryButtonStyle + }, + inputAccessoryView: { + } + ) + .preferredColorScheme(.dark) + .padding(.horizontal, 50) + .applyScreenBackground() + .previewLayout(.fixed(width: 500, height: 200)) + + SingleLineTextField( + placeholderText: "", + title: "Address", + store: .address, + titleAccessoryView: { + }, + inputAccessoryView: { + Image(Asset.Assets.Icons.qrCode.name) + .resizable() + .frame(width: 30, height: 30) + } + ) + .preferredColorScheme(.dark) + .padding(.horizontal, 50) + .applyScreenBackground() + .previewLayout(.fixed(width: 500, height: 200)) } } diff --git a/secant/Wrappers/WrappedNotificationCenter.swift b/secant/Wrappers/WrappedNotificationCenter.swift new file mode 100644 index 0000000..044df2b --- /dev/null +++ b/secant/Wrappers/WrappedNotificationCenter.swift @@ -0,0 +1,22 @@ +// +// WrappedNotificationCenter.swift +// secant-testnet +// +// Created by Lukรกลก Korba on 04.05.2022. +// + +import Foundation + +struct WrappedNotificationCenter { + let publisherFor: (Notification.Name) -> NotificationCenter.Publisher? +} + +extension WrappedNotificationCenter { + static let live = WrappedNotificationCenter( + publisherFor: { NotificationCenter.default.publisher(for: $0) } + ) + + static let mock = WrappedNotificationCenter( + publisherFor: { _ in nil } + ) +} diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index e512f2f..14ccae4 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -40,11 +40,12 @@ struct Balance: WalletBalance, Equatable { protocol WrappedSDKSynchronizer { var synchronizer: SDKSynchronizer? { get } var stateChanged: CurrentValueSubject { get } + var notificationCenter: WrappedNotificationCenter { get } func prepareWith(initializer: Initializer) throws func start(retry: Bool) throws func stop() - func synchronizerSynced() + func status() -> String func getShieldedBalance() -> Effect func getAllClearedTransactions() -> Effect<[TransactionState], Never> @@ -81,8 +82,10 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { private var cancellables: [AnyCancellable] = [] private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject + private(set) var notificationCenter: WrappedNotificationCenter - init() { + init(notificationCenter: WrappedNotificationCenter = .live) { + self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } @@ -93,16 +96,29 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { func prepareWith(initializer: Initializer) throws { synchronizer = try SDKSynchronizer(initializer: initializer) - NotificationCenter.default.publisher(for: .synchronizerSynced) + notificationCenter.publisherFor(.synchronizerStarted)? .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] _ in - self?.synchronizerSynced() - }) + .sink { [weak self] _ in self?.synchronizerStarted() } .store(in: &cancellables) - + + notificationCenter.publisherFor(.synchronizerSynced)? + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.synchronizerSynced() } + .store(in: &cancellables) + + notificationCenter.publisherFor(.synchronizerProgressUpdated)? + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.synchronizerProgressUpdated() } + .store(in: &cancellables) + + notificationCenter.publisherFor(.synchronizerStopped)? + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.synchronizerStopped() } + .store(in: &cancellables) + try synchronizer?.prepare() } - + func start(retry: Bool) throws { try synchronizer?.start(retry: retry) } @@ -111,10 +127,30 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { synchronizer?.stop() } + func synchronizerStarted() { + stateChanged.send(.started) + } + func synchronizerSynced() { stateChanged.send(.synced) } + func synchronizerProgressUpdated() { + stateChanged.send(.progressUpdated) + } + + func synchronizerStopped() { + stateChanged.send(.stopped) + } + + func status() -> String { + guard let synchronizer = synchronizer else { + return "" + } + + return SDKSynchronizer.textFor(state: synchronizer.status) + } + func getShieldedBalance() -> Effect { if let shieldedVerifiedBalance = synchronizer?.getShieldedVerifiedBalance(), let shieldedTotalBalance = synchronizer?.getShieldedBalance(accountIndex: 0) { @@ -215,11 +251,13 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { private var cancellables: [AnyCancellable] = [] private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject + private(set) var notificationCenter: WrappedNotificationCenter - init() { + init(notificationCenter: WrappedNotificationCenter = .mock) { + self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } - + deinit { synchronizer?.stop() } @@ -249,6 +287,14 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { stateChanged.send(.synced) } + func status() -> String { + guard let synchronizer = synchronizer else { + return "" + } + + return SDKSynchronizer.textFor(state: synchronizer.status) + } + func getShieldedBalance() -> Effect { return Effect(value: Balance(verified: 12345000, total: 12345000)) } @@ -339,8 +385,10 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject + private(set) var notificationCenter: WrappedNotificationCenter - init() { + init(notificationCenter: WrappedNotificationCenter = .mock) { + self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } @@ -352,6 +400,8 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func synchronizerSynced() { } + func status() -> String { "" } + func getShieldedBalance() -> Effect { return .none } diff --git a/secantTests/UtilTests/UserPreferencesStorageTests.swift b/secantTests/UtilTests/UserPreferencesStorageTests.swift index 95c250d..6bc67e7 100644 --- a/secantTests/UtilTests/UserPreferencesStorageTests.swift +++ b/secantTests/UtilTests/UserPreferencesStorageTests.swift @@ -19,7 +19,7 @@ class UserPreferencesStorageTests: XCTestCase { super.setUp() guard let userDefaults = UserDefaults.init(suiteName: "test") else { - XCTFail("UserPreferencesStorageTests: UserDefaults.init(suiteName: "test") failed to initialize") + XCTFail("UserPreferencesStorageTests: UserDefaults.init(suiteName: \"test\") failed to initialize") return } @@ -214,7 +214,7 @@ class UserPreferencesStorageTests: XCTestCase { func testRemoveAll() throws { guard let userDefaults = UserDefaults.init(suiteName: "test") else { - XCTFail("User Preferences: UserDefaults.init(suiteName: "test") failed to initialize") + XCTFail("User Preferences: UserDefaults.init(suiteName: \"test\") failed to initialize") return }