Add Forgot PIN button to AppLockSetupPINScreen. (#2833)

This commit is contained in:
Doug
2024-05-13 15:41:35 +01:00
committed by GitHub
parent 3a95868d77
commit 29ca045c5f
16 changed files with 89 additions and 35 deletions

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:640a83ceb101623de31fd268475af5b9059c7a2ca7a8f7aa8199fb7daef5a939
size 100288
oid sha256:f47aec44b9792c08b8d75c669bbc0dd4b3bf970817cef690507335244b59a4a7
size 107982

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1bbe9c2e6a8fb461ea4aaa0624f5f73c4465072b598d5aafd053df719eb5610
size 98946
oid sha256:46229a5d25d747e7f430b5123f1bf7c5e3d0bd51f5dc504362cf5af7eba49433
size 106665

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5de5005d6e79e1d8bbc85ec321df1978098c1caa08d951eb600e50348101f9b3
size 114059
oid sha256:9bf783d76f373076fe6cccf7c0edfd8d4b24b37852850eccdcc730215083d585
size 129004

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65ae23a0ac8ec3fe8ba1e8492c9347d0a0e57de0c2b1d1b84507ec9f0058a3e1
size 113353
oid sha256:e20edd9615c6ccea143b3d2e737a99d4f6aa216f4e8d98e7614cdc4fe5ba68b2
size 128143

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77162b478466530aa5673acca394d3b34b36174a486abc2aa78802b63dcccc04
size 57653
oid sha256:54fc37f67ea17e13a71c9f3f6d34c6070aa975b5bed4eeb41b43b1dd27b0b546
size 64943

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5de6d19acc089cea0d13048ea24ad2bdfbf84474f99469c97ad46f7a2135b58
size 56491
oid sha256:fb76fd47f5c34d06fc4e5ceb5c016ce218dbc48e731717c649eda583c70c6941
size 63822

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e79c31390ca36a329d1bebaa344a2aaaa16325d128c8c38275bc90a1b1da5047
size 70183
oid sha256:8b0676f18635f34b897fe405d32fe85dd6af30712781422a43ff2999f3b397e9
size 83873

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a338698467bdd325c3ac3e9ab714bcf3dd45f4f0cc1d4cb9cc7ace6a5c802237
size 69005
oid sha256:0230fed35ac76ef19c1cc0d0b2bc1828ecb995c9c5fdc18ee2342614162c0da4
size 82786

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abce176e4bf57b6b0aca2969a2f46675d5035d26ac975c7892e7ae9b26ba3827
size 112269
oid sha256:e08db1c88352a838368599fd9aca7ddddec87a40449a06c2d206e815da2a0cd8
size 115776

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b810dc56cb0b425f5da8b4ac3d30174ced03de8af3294929da1c6211a1d9b2e5
size 77122
oid sha256:31d7b840450d86e0bb8d585e901e8eb46673e5cbfd96e765e217b2fcdd5fb37f
size 83315

View File

@@ -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 {

View File

@@ -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)

1
changelog.d/2688.bugfix Normal file
View File

@@ -0,0 +1 @@
Add missing Forgot PIN option when asked to unlock the Screen Lock settings.