diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index d56e74f34..064b4bb09 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -14154,15 +14154,15 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol, @unchecked Sen } //MARK: - waitForKeyBackupUpload - var waitForKeyBackupUploadUnderlyingCallsCount = 0 - var waitForKeyBackupUploadCallsCount: Int { + var waitForKeyBackupUploadUploadStateSubjectUnderlyingCallsCount = 0 + var waitForKeyBackupUploadUploadStateSubjectCallsCount: Int { get { if Thread.isMainThread { - return waitForKeyBackupUploadUnderlyingCallsCount + return waitForKeyBackupUploadUploadStateSubjectUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = waitForKeyBackupUploadUnderlyingCallsCount + returnValue = waitForKeyBackupUploadUploadStateSubjectUnderlyingCallsCount } return returnValue! @@ -14170,27 +14170,29 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol, @unchecked Sen } set { if Thread.isMainThread { - waitForKeyBackupUploadUnderlyingCallsCount = newValue + waitForKeyBackupUploadUploadStateSubjectUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - waitForKeyBackupUploadUnderlyingCallsCount = newValue + waitForKeyBackupUploadUploadStateSubjectUnderlyingCallsCount = newValue } } } } - var waitForKeyBackupUploadCalled: Bool { - return waitForKeyBackupUploadCallsCount > 0 + var waitForKeyBackupUploadUploadStateSubjectCalled: Bool { + return waitForKeyBackupUploadUploadStateSubjectCallsCount > 0 } + var waitForKeyBackupUploadUploadStateSubjectReceivedUploadStateSubject: CurrentValueSubject? + var waitForKeyBackupUploadUploadStateSubjectReceivedInvocations: [CurrentValueSubject] = [] - var waitForKeyBackupUploadUnderlyingReturnValue: Result! - var waitForKeyBackupUploadReturnValue: Result! { + var waitForKeyBackupUploadUploadStateSubjectUnderlyingReturnValue: Result! + var waitForKeyBackupUploadUploadStateSubjectReturnValue: Result! { get { if Thread.isMainThread { - return waitForKeyBackupUploadUnderlyingReturnValue + return waitForKeyBackupUploadUploadStateSubjectUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = waitForKeyBackupUploadUnderlyingReturnValue + returnValue = waitForKeyBackupUploadUploadStateSubjectUnderlyingReturnValue } return returnValue! @@ -14198,22 +14200,26 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol, @unchecked Sen } set { if Thread.isMainThread { - waitForKeyBackupUploadUnderlyingReturnValue = newValue + waitForKeyBackupUploadUploadStateSubjectUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - waitForKeyBackupUploadUnderlyingReturnValue = newValue + waitForKeyBackupUploadUploadStateSubjectUnderlyingReturnValue = newValue } } } } - var waitForKeyBackupUploadClosure: (() async -> Result)? + var waitForKeyBackupUploadUploadStateSubjectClosure: ((CurrentValueSubject) async -> Result)? - func waitForKeyBackupUpload() async -> Result { - waitForKeyBackupUploadCallsCount += 1 - if let waitForKeyBackupUploadClosure = waitForKeyBackupUploadClosure { - return await waitForKeyBackupUploadClosure() + func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject) async -> Result { + waitForKeyBackupUploadUploadStateSubjectCallsCount += 1 + waitForKeyBackupUploadUploadStateSubjectReceivedUploadStateSubject = uploadStateSubject + DispatchQueue.main.async { + self.waitForKeyBackupUploadUploadStateSubjectReceivedInvocations.append(uploadStateSubject) + } + if let waitForKeyBackupUploadUploadStateSubjectClosure = waitForKeyBackupUploadUploadStateSubjectClosure { + return await waitForKeyBackupUploadUploadStateSubjectClosure(uploadStateSubject) } else { - return waitForKeyBackupUploadReturnValue + return waitForKeyBackupUploadUploadStateSubjectReturnValue } } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenModels.swift index 5fe201239..266f0755f 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenModels.swift @@ -13,9 +13,10 @@ enum SecureBackupLogoutConfirmationScreenViewModelAction { case logout } -enum SecureBackupLogoutConfirmationScreenViewMode { +enum SecureBackupLogoutConfirmationScreenViewMode: Equatable { case saveRecoveryKey - case backupOngoing + case waitingToStart(hasStalled: Bool) + case backupOngoing(progress: Double) case offline } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift index 1cec76544..699d53631 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift @@ -14,9 +14,13 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat private let secureBackupController: SecureBackupControllerProtocol private let appMediator: AppMediatorProtocol + private let backupUploadStateSubject: CurrentValueSubject = .init(.waiting) + // periphery:ignore - auto cancels when reassigned @CancellableTask private var keyUploadWaitingTask: Task? + @CancellableTask + private var keyUploadStalledTask: Task? private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -29,19 +33,11 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat super.init(initialViewState: .init(mode: .saveRecoveryKey)) - appMediator.networkMonitor.reachabilityPublisher + backupUploadStateSubject.combineLatest(appMediator.networkMonitor.reachabilityPublisher) .receive(on: DispatchQueue.main) - .sink { [weak self] reachability in - guard let self, - state.mode != .saveRecoveryKey else { - return - } - - if reachability == .reachable { - state.mode = .backupOngoing - } else { - state.mode = .offline - } + .sink { [weak self] backupState, reachability in + guard let self, state.mode != .saveRecoveryKey else { return } + updateMode(backupState: backupState, reachability: reachability) } .store(in: &cancellables) } @@ -65,29 +61,56 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat // MARK: - Private private func attemptLogout() { - if state.mode == .saveRecoveryKey { - state.mode = appMediator.networkMonitor.reachabilityPublisher.value == .reachable ? .backupOngoing : .offline + if case .saveRecoveryKey = state.mode { + updateMode(backupState: backupUploadStateSubject.value, reachability: appMediator.networkMonitor.reachabilityPublisher.value) keyUploadWaitingTask = Task { - var result = await secureBackupController.waitForKeyBackupUpload() + var result = await secureBackupController.waitForKeyBackupUpload(uploadStateSubject: backupUploadStateSubject) + + guard !Task.isCancelled else { return } if case .failure = result { // Retry the upload first, conditions might have changed. - result = await secureBackupController.waitForKeyBackupUpload() + result = await secureBackupController.waitForKeyBackupUpload(uploadStateSubject: backupUploadStateSubject) } + guard !Task.isCancelled else { return } + guard case .success = result else { MXLog.error("Aborting logout due to failure waiting for backup upload.") state.bindings.alertInfo = .init(id: .backupUploadFailed) return } - guard !Task.isCancelled else { return } - actionsSubject.send(.logout) } } else { actionsSubject.send(.logout) } } + + private func updateMode(backupState: SecureBackupSteadyState, reachability: NetworkMonitorReachability) { + switch (backupState, reachability) { + case (.waiting, .reachable): + state.mode = .waitingToStart(hasStalled: false) + showAsStalledAfterTimeout() + case (.uploading(let uploadedKeyCount, let totalKeyCount), .reachable): + state.mode = .backupOngoing(progress: Double(uploadedKeyCount) / Double(totalKeyCount)) + case (.error, .reachable): + break // Nothing to do here, it will be handled with the result. + case (.done, .reachable): + state.mode = .backupOngoing(progress: 1.0) + case (_, .unreachable): + state.mode = .offline + } + } + + /// If we stay in the waiting state for more than 2-seconds we ask the user to check their connection. + private func showAsStalledAfterTimeout() { + keyUploadStalledTask = Task { [weak self] in + try await Task.sleep(for: .seconds(2)) + guard let self, case .waitingToStart(hasStalled: false) = state.mode else { return } + state.mode = .waitingToStart(hasStalled: true) + } + } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift index 6f8c1fded..82406024c 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift @@ -40,16 +40,25 @@ struct SecureBackupLogoutConfirmationScreen: View { .font(.compound.bodyMD) .multilineTextAlignment(.center) - if context.viewState.mode == .backupOngoing { + if case let .waitingToStart(hasStalled) = context.viewState.mode { Spacer() ProgressView() + + if hasStalled { + Text(L10n.commonPleaseCheckInternetConnection) + .font(.compound.bodySM) + .foregroundColor(.compound.textPrimary) + } + } else if case let .backupOngoing(progress) = context.viewState.mode { + Spacer() + ProgressView(value: progress) } } @ViewBuilder private var footer: some View { VStack(spacing: 16.0) { - if context.viewState.mode == .saveRecoveryKey { + if case .saveRecoveryKey = context.viewState.mode { Button { context.send(viewAction: .settings) } label: { @@ -80,7 +89,7 @@ struct SecureBackupLogoutConfirmationScreen: View { switch context.viewState.mode { case .saveRecoveryKey: return L10n.screenSignoutSaveRecoveryKeyTitle - case .backupOngoing: + case .waitingToStart, .backupOngoing: return L10n.screenSignoutKeyBackupOngoingTitle case .offline: return L10n.screenSignoutKeyBackupOfflineTitle @@ -91,7 +100,7 @@ struct SecureBackupLogoutConfirmationScreen: View { switch context.viewState.mode { case .saveRecoveryKey: return L10n.screenSignoutSaveRecoveryKeySubtitle - case .backupOngoing: + case .waitingToStart, .backupOngoing: return L10n.screenSignoutKeyBackupOngoingSubtitle case .offline: return L10n.screenSignoutKeyBackupOfflineSubtitle @@ -102,25 +111,69 @@ struct SecureBackupLogoutConfirmationScreen: View { // MARK: - Previews struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = buildViewModel() + static let viewModel = makeViewModel(mode: .saveRecoveryKey) + static let waitingViewModel = makeViewModel(mode: .waitingToStart(hasStalled: false)) + static let ongoingViewModel = makeViewModel(mode: .backupOngoing(progress: 0.5)) + static let offlineViewModel = makeViewModel(mode: .offline) static var previews: some View { NavigationStack { SecureBackupLogoutConfirmationScreen(context: viewModel.context) } + .previewDisplayName("Confirmation") + + NavigationStack { + SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context) + } + .previewDisplayName("Waiting") + .snapshotPreferences(expect: waitingViewModel.context.$viewState.map { $0.mode == .waitingToStart(hasStalled: false) }) + + NavigationStack { + SecureBackupLogoutConfirmationScreen(context: ongoingViewModel.context) + } + .previewDisplayName("Ongoing") + .snapshotPreferences(expect: ongoingViewModel.context.$viewState.map { $0.mode == .backupOngoing(progress: 0.5) }) + + // Uses the same view model as Waiting but with a different expectation. + NavigationStack { + SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context) + } + .previewDisplayName("Stalled") + .snapshotPreferences(expect: waitingViewModel.context.$viewState.map { $0.mode == .waitingToStart(hasStalled: true) }) + + NavigationStack { + SecureBackupLogoutConfirmationScreen(context: offlineViewModel.context) + } + .previewDisplayName("Offline") + .snapshotPreferences(expect: offlineViewModel.context.$viewState.map { $0.mode == .offline }) } - static func buildViewModel() -> SecureBackupLogoutConfirmationScreenViewModelType { + static func makeViewModel(mode: SecureBackupLogoutConfirmationScreenViewMode) -> SecureBackupLogoutConfirmationScreenViewModel { let secureBackupController = SecureBackupControllerMock() secureBackupController.underlyingKeyBackupState = CurrentValueSubject(.enabled).asCurrentValuePublisher() + secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { uploadStateSubject in + if case .backupOngoing = mode { + uploadStateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100)) + } + + return .success(()) + } + + let reachability: NetworkMonitorReachability = mode == .offline ? .unreachable : .reachable let networkMonitor = NetworkMonitorMock() - networkMonitor.underlyingReachabilityPublisher = CurrentValueSubject(.reachable).asCurrentValuePublisher() + networkMonitor.underlyingReachabilityPublisher = CurrentValueSubject(reachability).asCurrentValuePublisher() let appMediator = AppMediatorMock() appMediator.underlyingNetworkMonitor = networkMonitor - return SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, - appMediator: appMediator) + let viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, + appMediator: appMediator) + + if mode != .saveRecoveryKey { + viewModel.context.send(viewAction: .logout) + } + + return viewModel } } diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift index b834059a2..ea350296b 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -35,7 +35,7 @@ class SecureBackupController: SecureBackupControllerProtocol { init(encryption: Encryption) { self.encryption = encryption - backupStateListenerTaskHandle = encryption.backupStateListener(listener: SecureBackupControllerBackupStateListener { [weak self] state in + backupStateListenerTaskHandle = encryption.backupStateListener(listener: SecureBackupControllerListener { [weak self] state in guard let self else { return } switch state { @@ -62,7 +62,7 @@ class SecureBackupController: SecureBackupControllerProtocol { } }) - recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SecureBackupRecoveryStateListener { [weak self] state in + recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SecureBackupControllerListener { [weak self] state in guard let self else { return } switch state { @@ -121,7 +121,7 @@ class SecureBackupController: SecureBackupControllerProtocol { MXLog.info("Enabling recovery") var keyUploadErrored = false - let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: nil, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in + let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: nil, progressListener: SecureBackupControllerListener { [weak self] state in guard let self else { return } switch state { @@ -154,10 +154,19 @@ class SecureBackupController: SecureBackupControllerProtocol { } } - func waitForKeyBackupUpload() async -> Result { + func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject) async -> Result { do { MXLog.info("Waiting for backup upload steady state") - try await encryption.waitForBackupUploadSteadyState(progressListener: nil) + try await encryption.waitForBackupUploadSteadyState(progressListener: SecureBackupControllerListener { state in + let uploadState: SecureBackupSteadyState = switch state { + case .waiting: .waiting + case .uploading(let backedUpCount, let totalCount): .uploading(uploadedKeyCount: Int(backedUpCount), totalKeyCount: Int(totalCount)) + case .error: .error + case .done: .done + } + + uploadStateSubject.send(uploadState) + }) return .success(()) } catch let error as SteadyStateError { MXLog.error("Failed waiting for backup upload steady state with error: \(error)") @@ -202,38 +211,19 @@ class SecureBackupController: SecureBackupControllerProtocol { } } -private final class SecureBackupControllerBackupStateListener: BackupStateListener { - private let onUpdateClosure: (BackupState) -> Void +private final class SecureBackupControllerListener { + private let onUpdateClosure: (T) -> Void - init(_ onUpdateClosure: @escaping (BackupState) -> Void) { + init(_ onUpdateClosure: @escaping (T) -> Void) { self.onUpdateClosure = onUpdateClosure } - func onUpdate(status: BackupState) { + func onUpdate(status: T) { onUpdateClosure(status) } } -private final class SecureBackupRecoveryStateListener: RecoveryStateListener { - private let onUpdateClosure: (RecoveryState) -> Void - - init(_ onUpdateClosure: @escaping (RecoveryState) -> Void) { - self.onUpdateClosure = onUpdateClosure - } - - func onUpdate(status: RecoveryState) { - onUpdateClosure(status) - } -} - -private final class SecureBackupEnableRecoveryProgressListener: EnableRecoveryProgressListener { - private let onUpdateClosure: (EnableRecoveryProgress) -> Void - - init(_ onUpdateClosure: @escaping (EnableRecoveryProgress) -> Void) { - self.onUpdateClosure = onUpdateClosure - } - - func onUpdate(status: EnableRecoveryProgress) { - onUpdateClosure(status) - } -} +extension SecureBackupControllerListener: BackupStateListener where T == BackupState { } +extension SecureBackupControllerListener: RecoveryStateListener where T == RecoveryState { } +extension SecureBackupControllerListener: EnableRecoveryProgressListener where T == EnableRecoveryProgress { } +extension SecureBackupControllerListener: BackupSteadyStateListener where T == BackupUploadState { } diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift index 3c90f209e..f2b1e9356 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift @@ -27,6 +27,14 @@ enum SecureBackupKeyBackupState { case disabling } +/// Represents the progress towards a complete backup before logging out. +enum SecureBackupSteadyState { + case waiting + case uploading(uploadedKeyCount: Int, totalKeyCount: Int) + case error + case done +} + enum SecureBackupControllerError: Error { case failedEnablingBackup case failedDisablingBackup @@ -49,5 +57,5 @@ protocol SecureBackupControllerProtocol { func generateRecoveryKey() async -> Result func confirmRecoveryKey(_ key: String) async -> Result - func waitForKeyBackupUpload() async -> Result + func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject) async -> Result } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPad-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPad-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPad-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPad-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPad-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPad-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPhone-16-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPhone-16-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPhone-16-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPhone-16-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.iPhone-16-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Confirmation-iPhone-16-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-en-GB.png new file mode 100644 index 000000000..5eb4aedd3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e19cfdfafb444821e7257e2af30fe59b51baa88bca0509a7a0f9509a527ca0ec +size 108492 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-pseudo.png new file mode 100644 index 000000000..04a5d4b3e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f909de428cb393d3dacecad51ee0d315046a1e81b990f56fc080ffb8d512c498 +size 134006 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-en-GB.png new file mode 100644 index 000000000..b86041115 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fd633dfede30f4f1967b339969ecae518b7ff94397f61cb35dd9729afe5db8c +size 67692 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-pseudo.png new file mode 100644 index 000000000..d21feca70 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Offline-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af96c6e11b09cb65fed81ee0cdb2499ddc730e009ae49d25ed097d38dfc36906 +size 95754 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-en-GB.png new file mode 100644 index 000000000..32fe67f94 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bce3908d36535874d68bc7020fb539c2208e9a8e5546d268dca5cb5fed2f1a07 +size 98088 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-pseudo.png new file mode 100644 index 000000000..5d2a3ada9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d265f547358fd98af6366212092955be02cbb625d0fb7a757bb0b128a1db7f1 +size 115611 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-en-GB.png new file mode 100644 index 000000000..8c1270115 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b043c9f56f2428f19a9559fe203494c2173c0668dcedde7170a01bb4821659d7 +size 56840 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-pseudo.png new file mode 100644 index 000000000..bb6574a8c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Ongoing-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0b304b1225cfc10709d5aeedb3ff786529f2732ac6175a8c272af204347eb21 +size 73926 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-en-GB.png new file mode 100644 index 000000000..f4a3b4cce --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7804f4e1e0c90dd67475eceda950491ae4bcf467ba118a3a4007672cd25545a +size 104266 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-pseudo.png new file mode 100644 index 000000000..89129167f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c34e8096228b99077da79d4caddc8bfec006ba13bf505fb38db0b4dcf1102dc +size 124631 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-en-GB.png new file mode 100644 index 000000000..909415bdc --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71b2003fc0485fc398312af8677d1b2f29774093ecf1c73ce72c88228cdbd3cb +size 62052 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-pseudo.png new file mode 100644 index 000000000..a188f2012 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Stalled-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1563190ef6c353223cafd89cc7a0d735e8e6b9ec69d4f9eccc7e13f4ccd0fe7 +size 84882 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-en-GB.png new file mode 100644 index 000000000..07ec24e43 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ae2a9e810bf4b66db4257c1bf9943a310f7f8ce8142eefb26ecf5c1fd31779f +size 98567 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-pseudo.png new file mode 100644 index 000000000..9b3ecadc3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24cc683908aea7c99604abc07537f38e53a46f517fde37e5877df642ab247b87 +size 115895 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-en-GB.png new file mode 100644 index 000000000..a006bb780 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85593f0e9ea14c615c870e3e721b2e0cdc96a9553d294d89702eacb6017d0582 +size 57151 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-pseudo.png new file mode 100644 index 000000000..7adcbb708 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/secureBackupLogoutConfirmationScreen.Waiting-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e2827f8d6ec2941e5116cb5bd72dc20b7286e88f639e2013e571991cefbac64 +size 74117 diff --git a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift index fbb89691e..8c30f3cea 100644 --- a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift @@ -5,9 +5,67 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import XCTest @testable import ElementX @MainActor -class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase { } +class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase { + var viewModel: SecureBackupLogoutConfirmationScreenViewModel! + var context: SecureBackupLogoutConfirmationScreenViewModel.Context { viewModel.context } + + var secureBackupController: SecureBackupControllerMock! + var reachabilitySubject: CurrentValueSubject! + + override func setUp() { + secureBackupController = SecureBackupControllerMock() + secureBackupController.underlyingKeyBackupState = CurrentValueSubject(.enabled).asCurrentValuePublisher() + + reachabilitySubject = CurrentValueSubject(.reachable) + let networkMonitor = NetworkMonitorMock() + networkMonitor.underlyingReachabilityPublisher = reachabilitySubject.asCurrentValuePublisher() + + let appMediator = AppMediatorMock() + appMediator.underlyingNetworkMonitor = networkMonitor + + viewModel = SecureBackupLogoutConfirmationScreenViewModel(secureBackupController: secureBackupController, + appMediator: appMediator) + } + + func testInitialState() { + XCTAssertEqual(context.viewState.mode, .saveRecoveryKey) + } + + func testOngoingState() async throws { + testInitialState() + + let progressExpectation = expectation(description: "The upload progress callback should be called.") + secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in + try? await Task.sleep(for: .seconds(4)) + stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100)) + progressExpectation.fulfill() + return .success(()) + } + + let deferredWaiting = deferFulfillment(context.$viewState) { $0.mode == .waitingToStart(hasStalled: false) } + context.send(viewAction: .logout) + try await deferredWaiting.fulfill() + + // Wait for the 2-second timeout. + let deferredHasStalled = deferFulfillment(context.$viewState) { $0.mode == .waitingToStart(hasStalled: true) } + try await deferredHasStalled.fulfill() + + // Wait for the progress to be reported. + await fulfillment(of: [progressExpectation]) + XCTAssertEqual(context.viewState.mode, .backupOngoing(progress: 0.5)) + } + + func testOfflineState() async throws { + try await testOngoingState() + + let deferred = deferFulfillment(context.$viewState) { $0.mode == .offline } + reachabilitySubject.send(.unreachable) + try await deferred.fulfill() + } +}