Add Forgot PIN button to AppLockSetupPINScreen. (#2833)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:640a83ceb101623de31fd268475af5b9059c7a2ca7a8f7aa8199fb7daef5a939
|
||||
size 100288
|
||||
oid sha256:f47aec44b9792c08b8d75c669bbc0dd4b3bf970817cef690507335244b59a4a7
|
||||
size 107982
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1bbe9c2e6a8fb461ea4aaa0624f5f73c4465072b598d5aafd053df719eb5610
|
||||
size 98946
|
||||
oid sha256:46229a5d25d747e7f430b5123f1bf7c5e3d0bd51f5dc504362cf5af7eba49433
|
||||
size 106665
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5de5005d6e79e1d8bbc85ec321df1978098c1caa08d951eb600e50348101f9b3
|
||||
size 114059
|
||||
oid sha256:9bf783d76f373076fe6cccf7c0edfd8d4b24b37852850eccdcc730215083d585
|
||||
size 129004
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:65ae23a0ac8ec3fe8ba1e8492c9347d0a0e57de0c2b1d1b84507ec9f0058a3e1
|
||||
size 113353
|
||||
oid sha256:e20edd9615c6ccea143b3d2e737a99d4f6aa216f4e8d98e7614cdc4fe5ba68b2
|
||||
size 128143
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:77162b478466530aa5673acca394d3b34b36174a486abc2aa78802b63dcccc04
|
||||
size 57653
|
||||
oid sha256:54fc37f67ea17e13a71c9f3f6d34c6070aa975b5bed4eeb41b43b1dd27b0b546
|
||||
size 64943
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5de6d19acc089cea0d13048ea24ad2bdfbf84474f99469c97ad46f7a2135b58
|
||||
size 56491
|
||||
oid sha256:fb76fd47f5c34d06fc4e5ceb5c016ce218dbc48e731717c649eda583c70c6941
|
||||
size 63822
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e79c31390ca36a329d1bebaa344a2aaaa16325d128c8c38275bc90a1b1da5047
|
||||
size 70183
|
||||
oid sha256:8b0676f18635f34b897fe405d32fe85dd6af30712781422a43ff2999f3b397e9
|
||||
size 83873
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a338698467bdd325c3ac3e9ab714bcf3dd45f4f0cc1d4cb9cc7ace6a5c802237
|
||||
size 69005
|
||||
oid sha256:0230fed35ac76ef19c1cc0d0b2bc1828ecb995c9c5fdc18ee2342614162c0da4
|
||||
size 82786
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abce176e4bf57b6b0aca2969a2f46675d5035d26ac975c7892e7ae9b26ba3827
|
||||
size 112269
|
||||
oid sha256:e08db1c88352a838368599fd9aca7ddddec87a40449a06c2d206e815da2a0cd8
|
||||
size 115776
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b810dc56cb0b425f5da8b4ac3d30174ced03de8af3294929da1c6211a1d9b2e5
|
||||
size 77122
|
||||
oid sha256:31d7b840450d86e0bb8d585e901e8eb46673e5cbfd96e765e217b2fcdd5fb37f
|
||||
size 83315
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
1
changelog.d/2688.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Add missing Forgot PIN option when asked to unlock the Screen Lock settings.
|
||||
Reference in New Issue
Block a user