diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ea8f7..d89570a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Changelog +# Unreleased +- [#611] Disable Send ZEC button when sync in progress # 0.0.1 build 44 This is the baseline build for iOS Re-Scoping epic. - [#819] build and release from tag 0.0.1-44 diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift index e3554af..ee9f1b2 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift @@ -12,19 +12,28 @@ import ZcashLightClientKit extension SDKSynchronizerDependency { static let mock: SDKSynchronizerClient = MockSDKSynchronizerClient() + + static func mockWithSnapshot(_ snapshot: SyncStatusSnapshot) -> MockSDKSynchronizerClient { + MockSDKSynchronizerClient(snapshot: snapshot) + } } class MockSDKSynchronizerClient: SDKSynchronizerClient { private var cancellables: [AnyCancellable] = [] + private var snapshot: SyncStatusSnapshot private(set) var notificationCenter: NotificationCenterClient private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var walletBirthday: BlockHeight? private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? - init(notificationCenter: NotificationCenterClient = .noOp) { + init( + notificationCenter: NotificationCenterClient = .noOp, + snapshot: SyncStatusSnapshot = .default + ) { self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) + self.snapshot = snapshot } func prepareWith(initializer: Initializer, seedBytes: [UInt8]) throws { } @@ -39,7 +48,7 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient { func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { } - func statusSnapshot() -> SyncStatusSnapshot { .default } + func statusSnapshot() -> SyncStatusSnapshot { self.snapshot } func rewind(_ policy: RewindPolicy) -> AnyPublisher? { Empty().eraseToAnyPublisher() } diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index 5e248d5..9dd42a5 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -53,6 +53,12 @@ struct HomeReducer: ReducerProtocol { } return false } + + var isSendButtonDisabled: Bool { + // If the destination is `.send` the button must be enabled + // to avoid involuntary navigation pop. + self.destination != .send && self.isSyncing + } } enum Action: Equatable { diff --git a/secant/Features/Home/HomeView.swift b/secant/Features/Home/HomeView.swift index 5d35c05..86d2834 100644 --- a/secant/Features/Home/HomeView.swift +++ b/secant/Features/Home/HomeView.swift @@ -82,6 +82,8 @@ extension HomeView { }) .activeButtonStyle .padding(.bottom, 30) + .disabled(viewStore.isSendButtonDisabled) + .opacity(viewStore.isSendButtonDisabled ? 0.5 : 1) } func receiveButton(_ viewStore: HomeViewStore) -> some View { diff --git a/secantTests/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift index 6c2b954..f23b6a6 100644 --- a/secantTests/HomeTests/HomeTests.swift +++ b/secantTests/HomeTests/HomeTests.swift @@ -6,9 +6,9 @@ // import XCTest -@testable import secant_testnet import ComposableArchitecture -import ZcashLightClientKit +@testable import secant_testnet +@testable import ZcashLightClientKit class HomeTests: XCTestCase { func testSynchronizerStateChanged_AnyButSynced() throws { @@ -32,7 +32,7 @@ class HomeTests: XCTestCase { reducer: HomeReducer() ) { dependencies in dependencies.mainQueue = testScheduler.eraseToAnyScheduler() - dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock + dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default) } store.send(.synchronizerStateChanged(.synced)) @@ -41,7 +41,92 @@ class HomeTests: XCTestCase { store.receive(.updateSynchronizerStatus) } - + + func testSendButtonIsDisabledWhenSyncing() { + let testScheduler = DispatchQueue.test + + let mockSnapshot = SyncStatusSnapshot.init( + .syncing( + .init( + startHeight: 1_700_000, + targetHeight: 1_800_000, + progressHeight: 1_770_000 + ) + ) + ) + + let store = TestStore( + initialState: .init( + balanceBreakdownState: .placeholder, + profileState: .placeholder, + scanState: .placeholder, + sendState: .placeholder, + settingsState: .placeholder, + shieldedBalance: Balance.zero, + synchronizerStatusSnapshot: mockSnapshot, + walletEventsState: .emptyPlaceHolder + ), + reducer: HomeReducer() + ) + + store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler() + store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot) + + store.send(.synchronizerStateChanged(.progressUpdated)) + + testScheduler.advance(by: 0.01) + + store.receive(.updateSynchronizerStatus) + + XCTAssertTrue(store.state.isSyncing) + XCTAssertTrue(store.state.isSendButtonDisabled) + } + + func testSendButtonIsNotDisabledWhenSyncingWhileOnSendScreen() { + let testScheduler = DispatchQueue.test + + let mockSnapshot = SyncStatusSnapshot.init( + .syncing( + .init( + startHeight: 1_700_000, + targetHeight: 1_800_000, + progressHeight: 1_770_000 + ) + ) + ) + + let store = TestStore( + initialState: .init( + balanceBreakdownState: .placeholder, + profileState: .placeholder, + scanState: .placeholder, + sendState: .placeholder, + settingsState: .placeholder, + shieldedBalance: Balance.zero, + synchronizerStatusSnapshot: mockSnapshot, + walletEventsState: .emptyPlaceHolder + ), + reducer: HomeReducer() + ) + + store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler() + store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot) + + store.send(.updateDestination(.send)) { + $0.destination = .send + } + + testScheduler.advance(by: 0.01) + + store.send(.synchronizerStateChanged(.progressUpdated)) + + testScheduler.advance(by: 0.01) + + store.receive(.updateSynchronizerStatus) + + XCTAssertTrue(store.state.isSyncing) + XCTAssertFalse(store.state.isSendButtonDisabled) + } /// The .onAppear action is important to register for the synchronizer state updates. /// The integration tests make sure registrations and side effects are properly implemented. func testOnAppear() throws { @@ -84,7 +169,7 @@ class HomeTests: XCTestCase { } // long-living (cancelable) effects need to be properly canceled. - // the .onDisappear action cancles the observer of the synchronizer status change. + // the .onDisappear action cancels the observer of the synchronizer status change. store.send(.onDisappear) } } diff --git a/secantTests/ProfileTests/ProfileTests.swift b/secantTests/ProfileTests/ProfileTests.swift index 99f345f..fd2111a 100644 --- a/secantTests/ProfileTests/ProfileTests.swift +++ b/secantTests/ProfileTests/ProfileTests.swift @@ -20,7 +20,7 @@ class ProfileTests: XCTestCase { reducer: ProfileReducer() ) { dependencies in dependencies.appVersion = .mock - dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock + dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default) } let uAddress = try UnifiedAddress( diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index b8ccbab..29bf9b7 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -50,7 +50,7 @@ class SendTests: XCTestCase { dependencies.derivationTool = .liveValue dependencies.mainQueue = testScheduler.eraseToAnyScheduler() dependencies.mnemonic = .liveValue - dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock + dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default) dependencies.walletStorage = .noOp } @@ -122,7 +122,7 @@ class SendTests: XCTestCase { dependencies.derivationTool = .liveValue dependencies.mainQueue = testScheduler.eraseToAnyScheduler() dependencies.mnemonic = .liveValue - dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock + dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default) dependencies.walletStorage = .noOp } diff --git a/secantTests/WalletEventsTests/WalletEventsTests.swift b/secantTests/WalletEventsTests/WalletEventsTests.swift index 42dda09..f4ba058 100644 --- a/secantTests/WalletEventsTests/WalletEventsTests.swift +++ b/secantTests/WalletEventsTests/WalletEventsTests.swift @@ -78,7 +78,7 @@ class WalletEventsTests: XCTestCase { reducer: WalletEventsFlowReducer() ) { dependencies in dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler() - dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock + dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default) } store.send(.synchronizerStateChanged(.synced))