Skip to content

Commit

Permalink
Enforce mandatory app lock outside of the authentication flow. (#1982)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave authored Oct 30, 2023
1 parent 8f01053 commit e9cdc76
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 20 deletions.
65 changes: 54 additions & 11 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,

private var authenticationCoordinator: AuthenticationCoordinator?
private let appLockFlowCoordinator: AppLockFlowCoordinator
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
private var softLogoutCoordinator: SoftLogoutScreenCoordinator?

Expand Down Expand Up @@ -143,7 +144,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
return
}

stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication)
guard userSessionStore.hasSessions else {
stateMachine.processEvent(.startWithAuthentication)
return
}

if appSettings.appLockFlowEnabled,
appSettings.appLockIsMandatory,
!appLockFlowCoordinator.appLockService.isEnabled {
stateMachine.processEvent(.startWithAppLockSetup)
} else {
stateMachine.processEvent(.startWithExistingSession)
}
}

func stop() {
Expand Down Expand Up @@ -319,28 +331,34 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,

switch (context.fromState, context.event, context.toState) {
case (.initial, .startWithAuthentication, .signedOut):
self.startAuthentication()
startAuthentication()
case (.signedOut, .createdUserSession, .signedIn):
self.setupUserSession()
setupUserSession()
case (.initial, .startWithExistingSession, .restoringSession):
self.restoreUserSession()
restoreUserSession()
case (.restoringSession, .failedRestoringSession, .signedOut):
self.showLoginErrorToast()
self.presentSplashScreen()
showLoginErrorToast()
presentSplashScreen()
case (.restoringSession, .createdUserSession, .signedIn):
self.setupUserSession()
setupUserSession()

case (.initial, .startWithAppLockSetup, .mandatoryAppLockSetup):
startMandatoryAppLockSetup()
case (.mandatoryAppLockSetup, .appLockSetupComplete, .restoringSession):
restoreUserSession()

case (.signingOut, .signOut, .signingOut):
// We can ignore signOut when already in the process of signing out,
// such as the SDK sending an authError due to token invalidation.
break
case (_, .signOut(let isSoft, _), .signingOut):
self.logout(isSoft: isSoft)
logout(isSoft: isSoft)
case (.signingOut(_, let disableAppLock), .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
case (.signingOut(_, let disableAppLock), .showSoftLogout, .softLogout):
self.presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
case (.signedIn, .clearCache, .initial):
self.clearCache()
clearCache()
default:
fatalError("Unknown transition: \(context)")
}
Expand Down Expand Up @@ -464,6 +482,31 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
}
}

/// Used to add a PIN code to an existing session that somehow missed out mandatory PIN setup.
private func startMandatoryAppLockSetup() {
MXLog.info("Mandatory App Lock enabled but no PIN is set. Showing the setup flow.")

let navigationCoordinator = NavigationStackCoordinator()
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .onboarding,
appLockService: appLockFlowCoordinator.appLockService,
navigationStackCoordinator: navigationCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
stateMachine.processEvent(.appLockSetupComplete)
appLockSetupFlowCoordinator = nil
case .forceLogout:
fatalError("Creating a PIN shouldn't be able to fail in this way")
}
}
.store(in: &cancellables)

appLockSetupFlowCoordinator = coordinator
navigationRootCoordinator.setRootCoordinator(navigationCoordinator)
coordinator.start()
}

private func logout(isSoft: Bool) {
guard let userSession else {
fatalError("User session not setup")
Expand Down
16 changes: 16 additions & 0 deletions ElementX/Sources/Application/AppCoordinatorStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class AppCoordinatorStateMachine {
/// Opening an existing session.
case restoringSession

/// Showing the mandatory app lock setup flow before restoring the session.
/// This state should only be allowed before restoring an existing session. For
/// new users the setup is inserted in the middle of the authentication flow.
case mandatoryAppLockSetup

/// User session started
case signedIn

Expand All @@ -44,12 +49,20 @@ class AppCoordinatorStateMachine {
/// Start the `AppCoordinator` by restoring an existing account.
case startWithExistingSession

/// Start the `AppCoordinator` by showing the mandatory PIN creation flow.
/// This event should only be sent if an account exists and a mandatory PIN is
/// missing. Normally it will be handled as part of the authentication flow.
case startWithAppLockSetup

/// Restoring session failed.
case failedRestoringSession

/// A session has been created.
case createdUserSession

/// The app lock setup has been completed.
case appLockSetupComplete

/// Request sign out.
case signOut(isSoft: Bool, disableAppLock: Bool)
/// Request the soft logout screen.
Expand Down Expand Up @@ -80,6 +93,9 @@ class AppCoordinatorStateMachine {
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])

stateMachine.addRoutes(event: .startWithAppLockSetup, transitions: [.initial => .mandatoryAppLockSetup])
stateMachine.addRoutes(event: .appLockSetupComplete, transitions: [.mandatoryAppLockSetup => .restoringSession])

stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false, disableAppLock: false) => .signedOut,
.signingOut(isSoft: false, disableAppLock: true) => .signedOut])
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true, disableAppLock: false) => .softLogout])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {

/// The presentation context of the flow.
enum PresentationFlow {
/// The flow is shown as for mandatory PIN creation in the authentication flow
case authentication
/// The flow is shown for mandatory PIN creation in the authentication flow or on app launch.
case onboarding
/// The flow is shown from the Settings screen.
case settings
}
Expand Down Expand Up @@ -113,7 +113,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {

switch (event, fromState) {
case (.start, .initial):
if presentingFlow == .authentication { return .createPIN(replacingExitingPIN: false) }
if presentingFlow == .onboarding { return .createPIN(replacingExitingPIN: false) }
return appLockService.isEnabled ? .unlock : .createPIN(replacingExitingPIN: false)
case (.pinEntered, .unlock):
return .settings
Expand All @@ -122,7 +122,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
case (.forceLogout, .unlock):
return .loggingOut
case (.pinEntered, .createPIN(let replacingExitingPIN)):
if presentingFlow == .authentication {
if presentingFlow == .onboarding {
return appLockService.biometryType != .none ? .biometricsPrompt : .complete
} else if !replacingExitingPIN {
return appLockService.biometricUnlockEnabled || appLockService.biometryType == .none ? .settings : .biometricsPrompt
Expand Down Expand Up @@ -182,7 +182,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
private func showCreatePIN() {
// Despite appLockService.isMandatory existing, we don't use that here,
// to allow for cancellation when changing the PIN code within settings.
let isMandatory = presentingFlow == .authentication
let isMandatory = presentingFlow == .onboarding

let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .create,
isMandatory: isMandatory,
Expand All @@ -200,8 +200,12 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)

if presentingFlow == .authentication {
navigationStackCoordinator.push(coordinator)
if presentingFlow == .onboarding {
if navigationStackCoordinator.rootCoordinator == nil {
navigationStackCoordinator.setRootCoordinator(coordinator)
} else {
navigationStackCoordinator.push(coordinator)
}
} else {
modalNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(modalNavigationStackCoordinator)
Expand All @@ -219,7 +223,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)

if presentingFlow == .authentication {
if presentingFlow == .onboarding {
navigationStackCoordinator.push(coordinator)
} else {
modalNavigationStackCoordinator.push(coordinator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
}

private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .authentication,
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .onboarding,
appLockService: appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
Expand Down
1 change: 1 addition & 0 deletions changelog.d/pr-1982.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enforce mandatory app lock outside of the authentication flow.

0 comments on commit e9cdc76

Please sign in to comment.