From 29ca045c5f18202f78d73f7e16dd52022aaaa348 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 13 May 2024 15:41:35 +0100 Subject: [PATCH] Add Forgot PIN button to AppLockSetupPINScreen. (#2833) --- .../AppLockSetupPINScreenModels.swift | 4 ++ .../AppLockSetupPINScreenViewModel.swift | 41 ++++++++++++------- .../View/AppLockSetupPINScreen.swift | 7 ++++ ...etupPINScreen-iPad-en-GB.Unlock-Failed.png | 4 +- ...ppLockSetupPINScreen-iPad-en-GB.Unlock.png | 4 +- ...tupPINScreen-iPad-pseudo.Unlock-Failed.png | 4 +- ...pLockSetupPINScreen-iPad-pseudo.Unlock.png | 4 +- ...INScreen-iPhone-15-en-GB.Unlock-Failed.png | 4 +- ...kSetupPINScreen-iPhone-15-en-GB.Unlock.png | 4 +- ...NScreen-iPhone-15-pseudo.Unlock-Failed.png | 4 +- ...SetupPINScreen-iPhone-15-pseudo.Unlock.png | 4 +- ...owUnlock-iPad-10th-generation-en-GB.UI.png | 4 +- ...LockSetupFlowUnlock-iPhone-15-en-GB.UI.png | 4 +- .../AppLock/AppLockScreenViewModelTests.swift | 9 +++- .../AppLockSetupPINScreenViewModelTests.swift | 22 ++++++++++ changelog.d/2688.bugfix | 1 + 16 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 changelog.d/2688.bugfix diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift index 72a404479..7c1a54af9 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift @@ -83,6 +83,8 @@ enum AppLockSetupPINScreenAlertType { case pinMismatch /// An error occurred setting the PIN code in the App Lock service. case failedToSetPIN + /// The user has forgotten their PIN, confirm they're happy to sign out. + case confirmResetPIN /// The user failed to unlock the app (or forgot their PIN). case forceLogout } @@ -90,4 +92,6 @@ enum AppLockSetupPINScreenAlertType { enum AppLockSetupPINScreenViewAction { /// Stop entering a PIN. case cancel + /// The user didn't heed the warnings and can't remember their PIN. + case forgotPIN } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift index 2a882f67d..563cd97b8 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift @@ -58,6 +58,8 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc switch viewAction { case .cancel: actionsSubject.send(.cancel) + case .forgotPIN: + displayAlert(.confirmResetPIN) } } @@ -80,7 +82,7 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc let pinCode = state.bindings.pinCode if case let .failure(error) = appLockService.validate(pinCode) { MXLog.warning("PIN rejected: \(error)") - handleError(.weakPIN) + displayAlert(.weakPIN) return } @@ -95,17 +97,17 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc let pinCode = state.bindings.pinCode guard pinCode == newPIN else { MXLog.warning("PIN mismatch.") - handleError(.pinMismatch) + displayAlert(.pinMismatch) return } if case let .failure(error) = appLockService.setupPINCode(pinCode) { MXLog.warning("Failed to set PIN: \(error)") if case .keychainError = error { - handleError(.failedToSetPIN) + displayAlert(.failedToSetPIN) return } else { - handleError(.weakPIN) // Shouldn't really happen but just in case. + displayAlert(.weakPIN) // Shouldn't really happen but just in case. return } } @@ -118,7 +120,7 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc guard appLockService.unlock(with: state.bindings.pinCode) else { state.bindings.pinCode = "" if state.numberOfUnlockAttempts >= state.maximumAttempts { - handleError(.forceLogout) + displayAlert(.forceLogout) } return } @@ -126,27 +128,33 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc actionsSubject.send(.complete) } - private func handleError(_ error: AppLockSetupPINScreenAlertType) { - switch error { + private func displayAlert(_ alertType: AppLockSetupPINScreenAlertType) { + switch alertType { case .weakPIN: - state.bindings.alertInfo = .init(id: error, + state.bindings.alertInfo = .init(id: alertType, title: L10n.screenAppLockSetupPinBlacklistedDialogTitle, message: L10n.screenAppLockSetupPinBlacklistedDialogContent, primaryButton: .init(title: L10n.actionOk) { self.state.bindings.pinCode = "" }) case .pinMismatch: state.numberOfConfirmAttempts += 1 - state.bindings.alertInfo = .init(id: error, + state.bindings.alertInfo = .init(id: alertType, title: L10n.screenAppLockSetupPinMismatchDialogTitle, message: L10n.screenAppLockSetupPinMismatchDialogContent, primaryButton: .init(title: L10n.actionTryAgain) { self.restartCreateIfNeeded() }) case .failedToSetPIN: - state.bindings.alertInfo = .init(id: error) - case .forceLogout: - state.isLoggingOut = true // Disable the screen before showing the alert. - state.bindings.alertInfo = .init(id: error, + state.bindings.alertInfo = .init(id: alertType) + case .confirmResetPIN: + state.bindings.alertInfo = .init(id: alertType, title: L10n.screenAppLockSignoutAlertTitle, message: L10n.screenAppLockSignoutAlertMessage, - primaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.forceLogout) }) + primaryButton: .init(title: L10n.actionOk) { self.forceLogout() }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + case .forceLogout: + state.isLoggingOut = true // Disable the screen before showing the alert. + state.bindings.alertInfo = .init(id: alertType, + title: L10n.screenAppLockSignoutAlertTitle, + message: L10n.screenAppLockSignoutAlertMessage, + primaryButton: .init(title: L10n.actionOk) { self.forceLogout() }) } } @@ -159,4 +167,9 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc state.numberOfConfirmAttempts = 0 } } + + private func forceLogout() { + state.isLoggingOut = true // Double call on failed to unlock, but not for forgot PIN. + actionsSubject.send(.forceLogout) + } } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift index 826815de6..1bb58c6a4 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift @@ -43,6 +43,13 @@ struct AppLockSetupPINScreen: View { PINTextField(pinCode: $context.pinCode, isSecure: true) .focused($textFieldFocus) + + if context.viewState.mode == .unlock { + Button(L10n.screenAppLockForgotPin) { + context.send(viewAction: .forgotPIN) + } + .buttonStyle(.compound(.plain)) + } } .padding(.horizontal, 16) .padding(.top, UIConstants.iconTopPaddingToNavigationBar) diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock-Failed.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock-Failed.png index 6ac162f7f..058d93b93 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock-Failed.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock-Failed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:640a83ceb101623de31fd268475af5b9059c7a2ca7a8f7aa8199fb7daef5a939 -size 100288 +oid sha256:f47aec44b9792c08b8d75c669bbc0dd4b3bf970817cef690507335244b59a4a7 +size 107982 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock.png index d27769cfb..8fc2e89b3 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-en-GB.Unlock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1bbe9c2e6a8fb461ea4aaa0624f5f73c4465072b598d5aafd053df719eb5610 -size 98946 +oid sha256:46229a5d25d747e7f430b5123f1bf7c5e3d0bd51f5dc504362cf5af7eba49433 +size 106665 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock-Failed.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock-Failed.png index 3dd6cd7ed..beeb872ab 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock-Failed.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock-Failed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5de5005d6e79e1d8bbc85ec321df1978098c1caa08d951eb600e50348101f9b3 -size 114059 +oid sha256:9bf783d76f373076fe6cccf7c0edfd8d4b24b37852850eccdcc730215083d585 +size 129004 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock.png index fcce47ff8..968280bec 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPad-pseudo.Unlock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65ae23a0ac8ec3fe8ba1e8492c9347d0a0e57de0c2b1d1b84507ec9f0058a3e1 -size 113353 +oid sha256:e20edd9615c6ccea143b3d2e737a99d4f6aa216f4e8d98e7614cdc4fe5ba68b2 +size 128143 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock-Failed.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock-Failed.png index dbb72ef85..5e0ac9659 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock-Failed.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock-Failed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77162b478466530aa5673acca394d3b34b36174a486abc2aa78802b63dcccc04 -size 57653 +oid sha256:54fc37f67ea17e13a71c9f3f6d34c6070aa975b5bed4eeb41b43b1dd27b0b546 +size 64943 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock.png index e40b4606e..f4336f39e 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-en-GB.Unlock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5de6d19acc089cea0d13048ea24ad2bdfbf84474f99469c97ad46f7a2135b58 -size 56491 +oid sha256:fb76fd47f5c34d06fc4e5ceb5c016ce218dbc48e731717c649eda583c70c6941 +size 63822 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock-Failed.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock-Failed.png index c8008d66a..802975e18 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock-Failed.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock-Failed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e79c31390ca36a329d1bebaa344a2aaaa16325d128c8c38275bc90a1b1da5047 -size 70183 +oid sha256:8b0676f18635f34b897fe405d32fe85dd6af30712781422a43ff2999f3b397e9 +size 83873 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock.png b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock.png index 323510631..5687f4f92 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen-iPhone-15-pseudo.Unlock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a338698467bdd325c3ac3e9ab714bcf3dd45f4f0cc1d4cb9cc7ace6a5c802237 -size 69005 +oid sha256:0230fed35ac76ef19c1cc0d0b2bc1828ecb995c9c5fdc18ee2342614162c0da4 +size 82786 diff --git a/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPad-10th-generation-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPad-10th-generation-en-GB.UI.png index 5ffc6acb7..5430bbb2e 100644 --- a/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPad-10th-generation-en-GB.UI.png +++ b/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPad-10th-generation-en-GB.UI.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abce176e4bf57b6b0aca2969a2f46675d5035d26ac975c7892e7ae9b26ba3827 -size 112269 +oid sha256:e08db1c88352a838368599fd9aca7ddddec87a40449a06c2d206e815da2a0cd8 +size 115776 diff --git a/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPhone-15-en-GB.UI.png b/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPhone-15-en-GB.UI.png index 70153dba4..5c0591497 100644 --- a/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPhone-15-en-GB.UI.png +++ b/UITests/Sources/__Snapshots__/Application/appLockSetupFlowUnlock-iPhone-15-en-GB.UI.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b810dc56cb0b425f5da8b4ac3d30174ced03de8af3294929da1c6211a1d9b2e5 -size 77122 +oid sha256:31d7b840450d86e0bb8d585e901e8eb46673e5cbfd96e765e217b2fcdd5fb37f +size 83315 diff --git a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift index d36cc0fb7..44b8411ea 100644 --- a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift @@ -54,7 +54,7 @@ class AppLockScreenViewModelTests: XCTestCase { XCTAssertEqual(result, .appUnlocked) } - func testForgotPIN() { + func testForgotPIN() async throws { // Given a fresh launch of the app. XCTAssertNil(context.alertInfo, "No alert should be shown initially.") @@ -63,6 +63,13 @@ class AppLockScreenViewModelTests: XCTestCase { // Then an alert should be shown before logging out. XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "An alert should be shown before logging out.") + + // When confirming the logout. + let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout } + context.alertInfo?.primaryButton.action?() + + // Then a force logout should be initiated. + try await deferred.fulfill() } func testUnlockFailure() async throws { diff --git a/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift index 1cc4043e8..9e00868df 100644 --- a/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift @@ -128,6 +128,28 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase { try await deferred.fulfill() } + func testForgotPIN() async throws { + // Given the screen in unlock mode. + viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService) + XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") + XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.") + + // When the user has forgotten their PIN. + context.send(viewAction: .forgotPIN) + + // Then an alert should be shown before logging out. + XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "The weak PIN should be rejected.") + XCTAssertFalse(context.viewState.isLoggingOut, "The view should not be disabled until the user confirms.") + + // When confirming the logout. + let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout } + context.alertInfo?.primaryButton.action?() + + // Then a force logout should be initiated. + try await deferred.fulfill() + XCTAssertTrue(context.viewState.isLoggingOut, "The view should become disabled.") + } + func testUnlockFailed() async throws { // Given the screen in unlock mode. viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService) diff --git a/changelog.d/2688.bugfix b/changelog.d/2688.bugfix new file mode 100644 index 000000000..1ad6d5cc5 --- /dev/null +++ b/changelog.d/2688.bugfix @@ -0,0 +1 @@ +Add missing Forgot PIN option when asked to unlock the Screen Lock settings. \ No newline at end of file