Make LinkNewDeviceScreen fall back to the App Lock PIN code when there's no passcode. (#5794)

* Fall back to the AppLock PIN code when there's no passcode.

* Add extra tests on the LinkNewDeviceFlowCoordinator.

* Misc tweaks for clarification.

* Regenerate snapshots.
This commit is contained in:
Doug
2026-07-01 10:13:18 +01:00
committed by GitHub
parent 7097ceb910
commit 123661537a
38 changed files with 296 additions and 21 deletions
@@ -43,6 +43,19 @@ final class AppLockScreenViewModelTests {
#expect(result == .appUnlocked)
}
@Test
func cancelVerifyDeviceOwner() async throws {
// Given a screen shown to verify the device owner.
viewModel = AppLockScreenViewModel(appLockService: appLockService, mode: .verifyDeviceOwner)
// When the user cancels.
let deferred = deferFulfillment(viewModel.actions) { $0 == .cancelVerifyDeviceOwner }
viewModel.context.send(viewAction: .cancelVerifyDeviceOwner)
// Then the cancellation should be reported.
try await deferred.fulfill()
}
@Test
func forgotPIN() async throws {
// Given a fresh launch of the app.
@@ -91,8 +91,8 @@ class LinkNewDeviceScreenViewModelTests {
}
@Test
func linkingProceedsWhenVerificationUnavailable() async throws {
// Given a device with no passcode set, so device owner verification is unavailable.
func linkingWithoutDevicePasscodeOrAppLockPIN() async throws {
// Given a device with neither a passcode nor an App Lock PIN set (device owner verification is unavailable).
let progressSubject = CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>(.starting)
setupViewModel(linkMobileProgressPublisher: progressSubject.asCurrentValuePublisher(),
deviceOwnerResult: .unavailable,
@@ -109,20 +109,56 @@ class LinkNewDeviceScreenViewModelTests {
}
@Test
func appLockPINRequiredDoesNotLink() async throws {
func appLockPINRequiredRequestsVerification() async throws {
// Given a device with no passcode but an App Lock PIN set, so PIN verification is required.
setupViewModel(deviceOwnerResult: .appLockPINRequired,
mode: .loading)
// When linking a mobile device, it should not proceed to generate a QR code.
// TODO: assert the App Lock PIN screen is presented once it's wired up.
let deferred = deferFailure(context.observe(\.viewState.mode), timeout: .seconds(1)) { $0 == .readyToLink(.generatingCode) }
// When linking a mobile device.
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0.isVerifyWithAppLockPIN }
context.send(viewAction: .linkMobileDevice)
try await deferred.fulfill()
let action = try await deferred.fulfill()
// Then verification should have been attempted but linking should not have started.
// Then it should request App Lock PIN verification rather than linking.
#expect(appLockService.verifyDeviceOwnerReasonCalled)
#expect(!linkNewDeviceService.linkMobileDeviceCalled)
// When cancelling the verification.
guard case let .verifyWithAppLockPIN(continuation) = action else {
Issue.record("Unexpected action.")
return
}
let deferredIdle = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .readyToLink(.idle) }
continuation.resume(returning: false)
// Then it should return to idle without linking.
try await deferredIdle.fulfill()
#expect(!linkNewDeviceService.linkMobileDeviceCalled)
}
@Test
func appLockPINVerifiedContinuesLinking() async throws {
// Given a device that requires App Lock PIN verification.
let progressSubject = CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>(.starting)
setupViewModel(linkMobileProgressPublisher: progressSubject.asCurrentValuePublisher(),
deviceOwnerResult: .appLockPINRequired,
mode: .loading)
// When linking a mobile device and the PIN verification succeeds.
let deferred = deferFulfillment(viewModel.actionsPublisher) { $0.isVerifyWithAppLockPIN }
context.send(viewAction: .linkMobileDevice)
let action = try await deferred.fulfill()
guard case let .verifyWithAppLockPIN(continuation) = action else {
Issue.record("Unexpected action.")
return
}
let deferredGenerating = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .readyToLink(.generatingCode) }
continuation.resume(returning: true)
// Then it should proceed to generate the QR code.
try await deferredGenerating.fulfill()
#expect(linkNewDeviceService.linkMobileDeviceCalled)
}
// MARK: - Verification failures
@@ -259,4 +295,11 @@ private extension LinkNewDeviceScreenViewModelAction {
default: false
}
}
var isVerifyWithAppLockPIN: Bool {
switch self {
case .verifyWithAppLockPIN: true
default: false
}
}
}