Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce mandatory app lock outside of the authentication flow. #1982

Merged
merged 3 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.