Ensure multiple mandatory verification flows can be ran consecutively (e.g. following encryption resets) (#3722)

* Ensure multiple mandatory verification flows can be ran consecutively (e.g. following encryption resets)

* Disabled the back button on the verification screen only when verified and waiting for the security state publisher
This commit is contained in:
Stefan Ceriu 2025-01-31 17:31:21 +02:00 committed by GitHub
parent 4856ffd3b2
commit 97069850f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 20 additions and 10 deletions

View File

@ -38,7 +38,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
enum Event: EventType { enum Event: EventType {
case next case next
case nextSkippingIdentityConfimed case nextSkippingIdentityConfirmed
} }
private let stateMachine: StateMachine<State, Event> private let stateMachine: StateMachine<State, Event>
@ -79,6 +79,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
stateMachine = .init(state: .initial) stateMachine = .init(state: .initial)
configureStateMachine()
// Verification can change as part of the onboarding flow by verifying with // Verification can change as part of the onboarding flow by verifying with
// another device, using a recovery key or by resetting one's crypto identity. // another device, using a recovery key or by resetting one's crypto identity.
// It can also happen that onboarding started before it had a chance to update, // It can also happen that onboarding started before it had a chance to update,
@ -86,15 +88,14 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
// Handle all those cases here instead of spreading them throughout the code. // Handle all those cases here instead of spreading them throughout the code.
verificationStateCancellable = userSession.sessionSecurityStatePublisher verificationStateCancellable = userSession.sessionSecurityStatePublisher
.map(\.verificationState) .map(\.verificationState)
.removeDuplicates() .receive(on: DispatchQueue.main)
.sink { [weak self] value in .sink { [weak self] value in
guard let self, guard let self,
value == .verified, value == .verified,
stateMachine.state == .identityConfirmation else { return } stateMachine.state == .identityConfirmation else { return }
appSettings.hasRunIdentityConfirmationOnboarding = true appSettings.hasRunIdentityConfirmationOnboarding = true
stateMachine.tryEvent(.nextSkippingIdentityConfimed) stateMachine.tryEvent(.nextSkippingIdentityConfirmed)
self.verificationStateCancellable = nil
} }
} }
@ -111,8 +112,6 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
fatalError("This flow coordinator shouldn't have been started") fatalError("This flow coordinator shouldn't have been started")
} }
configureStateMachine()
rootNavigationStackCoordinator.setFullScreenCoverCoordinator(navigationStackCoordinator, animated: !isNewLogin) rootNavigationStackCoordinator.setFullScreenCoverCoordinator(navigationStackCoordinator, animated: !isNewLogin)
stateMachine.tryEvent(.next) stateMachine.tryEvent(.next)
@ -146,6 +145,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
} }
private func configureStateMachine() { private func configureStateMachine() {
stateMachine.addRoute(.init(fromState: .finished, toState: .initial))
stateMachine.addRouteMapping { [weak self] event, fromState, _ in stateMachine.addRouteMapping { [weak self] event, fromState, _ in
guard let self else { guard let self else {
return nil return nil
@ -164,7 +164,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
return .finished return .finished
case (.identityConfirmation, _, _, _, _): case (.identityConfirmation, _, _, _, _):
if event == .nextSkippingIdentityConfimed { if event == .nextSkippingIdentityConfirmed {
// Used when the verification state has updated to verified // Used when the verification state has updated to verified
// after starting the onboarding flow // after starting the onboarding flow
switch (requiresAppLockSetup, requiresAnalyticsSetup, requiresNotificationsSetup) { switch (requiresAppLockSetup, requiresAnalyticsSetup, requiresNotificationsSetup) {
@ -225,9 +225,18 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
presentNotificationPermissionsScreen() presentNotificationPermissionsScreen()
case (_, _, .finished): case (_, _, .finished):
rootNavigationStackCoordinator.setFullScreenCoverCoordinator(nil) rootNavigationStackCoordinator.setFullScreenCoverCoordinator(nil)
stateMachine.tryState(.initial)
case (.finished, _, .initial):
break
default: default:
fatalError("Unknown transition: \(context)") fatalError("Unknown transition: \(context)")
} }
if let event = context.event {
MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(event)`")
} else {
MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`")
}
} }
stateMachine.addErrorHandler { context in stateMachine.addErrorHandler { context in
@ -251,7 +260,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
presentRecoveryKeyScreen() presentRecoveryKeyScreen()
case .skip: case .skip:
appSettings.hasRunIdentityConfirmationOnboarding = true appSettings.hasRunIdentityConfirmationOnboarding = true
stateMachine.tryEvent(.nextSkippingIdentityConfimed) stateMachine.tryEvent(.nextSkippingIdentityConfirmed)
case .reset: case .reset:
startEncryptionResetFlow() startEncryptionResetFlow()
case .logout: case .logout:

View File

@ -220,6 +220,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
func attemptStartingOnboarding() { func attemptStartingOnboarding() {
MXLog.info("Attempting to start onboarding")
if onboardingFlowCoordinator.shouldStart { if onboardingFlowCoordinator.shouldStart {
clearRoute(animated: false) clearRoute(animated: false)
onboardingFlowCoordinator.start() onboardingFlowCoordinator.start()
@ -340,7 +342,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
userSession.sessionSecurityStatePublisher userSession.sessionSecurityStatePublisher
.map(\.verificationState) .map(\.verificationState)
.filter { $0 != .unknown } .filter { $0 != .unknown }
.removeDuplicates()
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }

View File

@ -24,6 +24,7 @@ struct SessionVerificationScreen: View {
.background() .background()
.backgroundStyle(.compound.bgCanvasDefault) .backgroundStyle(.compound.bgCanvasDefault)
.interactiveDismissDisabled() .interactiveDismissDisabled()
.navigationBarBackButtonHidden(context.viewState.verificationState == .verified)
} }
// MARK: - Private // MARK: - Private

View File

@ -47,7 +47,6 @@ class UserSession: UserSessionProtocol {
MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)") MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)")
return SessionSecurityState(verificationState: $0, recoveryState: $1) return SessionSecurityState(verificationState: $0, recoveryState: $1)
} }
.removeDuplicates()
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] value in .sink { [weak self] value in
self?.sessionSecurityStateSubject.send(value) self?.sessionSecurityStateSubject.send(value)