mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Enforce mandatory app lock outside of the authentication flow. (#1982)
This commit is contained in:
@@ -45,6 +45,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
|||||||
|
|
||||||
private var authenticationCoordinator: AuthenticationCoordinator?
|
private var authenticationCoordinator: AuthenticationCoordinator?
|
||||||
private let appLockFlowCoordinator: AppLockFlowCoordinator
|
private let appLockFlowCoordinator: AppLockFlowCoordinator
|
||||||
|
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||||
private var softLogoutCoordinator: SoftLogoutScreenCoordinator?
|
private var softLogoutCoordinator: SoftLogoutScreenCoordinator?
|
||||||
|
|
||||||
@@ -143,7 +144,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
|||||||
return
|
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() {
|
func stop() {
|
||||||
@@ -319,28 +331,34 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
|||||||
|
|
||||||
switch (context.fromState, context.event, context.toState) {
|
switch (context.fromState, context.event, context.toState) {
|
||||||
case (.initial, .startWithAuthentication, .signedOut):
|
case (.initial, .startWithAuthentication, .signedOut):
|
||||||
self.startAuthentication()
|
startAuthentication()
|
||||||
case (.signedOut, .createdUserSession, .signedIn):
|
case (.signedOut, .createdUserSession, .signedIn):
|
||||||
self.setupUserSession()
|
setupUserSession()
|
||||||
case (.initial, .startWithExistingSession, .restoringSession):
|
case (.initial, .startWithExistingSession, .restoringSession):
|
||||||
self.restoreUserSession()
|
restoreUserSession()
|
||||||
case (.restoringSession, .failedRestoringSession, .signedOut):
|
case (.restoringSession, .failedRestoringSession, .signedOut):
|
||||||
self.showLoginErrorToast()
|
showLoginErrorToast()
|
||||||
self.presentSplashScreen()
|
presentSplashScreen()
|
||||||
case (.restoringSession, .createdUserSession, .signedIn):
|
case (.restoringSession, .createdUserSession, .signedIn):
|
||||||
self.setupUserSession()
|
setupUserSession()
|
||||||
|
|
||||||
|
case (.initial, .startWithAppLockSetup, .mandatoryAppLockSetup):
|
||||||
|
startMandatoryAppLockSetup()
|
||||||
|
case (.mandatoryAppLockSetup, .appLockSetupComplete, .restoringSession):
|
||||||
|
restoreUserSession()
|
||||||
|
|
||||||
case (.signingOut, .signOut, .signingOut):
|
case (.signingOut, .signOut, .signingOut):
|
||||||
// We can ignore signOut when already in the process of signing out,
|
// We can ignore signOut when already in the process of signing out,
|
||||||
// such as the SDK sending an authError due to token invalidation.
|
// such as the SDK sending an authError due to token invalidation.
|
||||||
break
|
break
|
||||||
case (_, .signOut(let isSoft, _), .signingOut):
|
case (_, .signOut(let isSoft, _), .signingOut):
|
||||||
self.logout(isSoft: isSoft)
|
logout(isSoft: isSoft)
|
||||||
case (.signingOut(_, let disableAppLock), .completedSigningOut, .signedOut):
|
case (.signingOut(_, let disableAppLock), .completedSigningOut, .signedOut):
|
||||||
self.presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
|
presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
|
||||||
case (.signingOut(_, let disableAppLock), .showSoftLogout, .softLogout):
|
case (.signingOut(_, let disableAppLock), .showSoftLogout, .softLogout):
|
||||||
self.presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
|
presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
|
||||||
case (.signedIn, .clearCache, .initial):
|
case (.signedIn, .clearCache, .initial):
|
||||||
self.clearCache()
|
clearCache()
|
||||||
default:
|
default:
|
||||||
fatalError("Unknown transition: \(context)")
|
fatalError("Unknown transition: \(context)")
|
||||||
}
|
}
|
||||||
@@ -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) {
|
private func logout(isSoft: Bool) {
|
||||||
guard let userSession else {
|
guard let userSession else {
|
||||||
fatalError("User session not setup")
|
fatalError("User session not setup")
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class AppCoordinatorStateMachine {
|
|||||||
/// Opening an existing session.
|
/// Opening an existing session.
|
||||||
case restoringSession
|
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
|
/// User session started
|
||||||
case signedIn
|
case signedIn
|
||||||
|
|
||||||
@@ -44,12 +49,20 @@ class AppCoordinatorStateMachine {
|
|||||||
/// Start the `AppCoordinator` by restoring an existing account.
|
/// Start the `AppCoordinator` by restoring an existing account.
|
||||||
case startWithExistingSession
|
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.
|
/// Restoring session failed.
|
||||||
case failedRestoringSession
|
case failedRestoringSession
|
||||||
|
|
||||||
/// A session has been created.
|
/// A session has been created.
|
||||||
case createdUserSession
|
case createdUserSession
|
||||||
|
|
||||||
|
/// The app lock setup has been completed.
|
||||||
|
case appLockSetupComplete
|
||||||
|
|
||||||
/// Request sign out.
|
/// Request sign out.
|
||||||
case signOut(isSoft: Bool, disableAppLock: Bool)
|
case signOut(isSoft: Bool, disableAppLock: Bool)
|
||||||
/// Request the soft logout screen.
|
/// Request the soft logout screen.
|
||||||
@@ -80,6 +93,9 @@ class AppCoordinatorStateMachine {
|
|||||||
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
|
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
|
||||||
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
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,
|
stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false, disableAppLock: false) => .signedOut,
|
||||||
.signingOut(isSoft: false, disableAppLock: true) => .signedOut])
|
.signingOut(isSoft: false, disableAppLock: true) => .signedOut])
|
||||||
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true, disableAppLock: false) => .softLogout])
|
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true, disableAppLock: false) => .softLogout])
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
/// The presentation context of the flow.
|
/// The presentation context of the flow.
|
||||||
enum PresentationFlow {
|
enum PresentationFlow {
|
||||||
/// The flow is shown as for mandatory PIN creation in the authentication flow
|
/// The flow is shown for mandatory PIN creation in the authentication flow or on app launch.
|
||||||
case authentication
|
case onboarding
|
||||||
/// The flow is shown from the Settings screen.
|
/// The flow is shown from the Settings screen.
|
||||||
case settings
|
case settings
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
|
|
||||||
switch (event, fromState) {
|
switch (event, fromState) {
|
||||||
case (.start, .initial):
|
case (.start, .initial):
|
||||||
if presentingFlow == .authentication { return .createPIN(replacingExitingPIN: false) }
|
if presentingFlow == .onboarding { return .createPIN(replacingExitingPIN: false) }
|
||||||
return appLockService.isEnabled ? .unlock : .createPIN(replacingExitingPIN: false)
|
return appLockService.isEnabled ? .unlock : .createPIN(replacingExitingPIN: false)
|
||||||
case (.pinEntered, .unlock):
|
case (.pinEntered, .unlock):
|
||||||
return .settings
|
return .settings
|
||||||
@@ -122,7 +122,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case (.forceLogout, .unlock):
|
case (.forceLogout, .unlock):
|
||||||
return .loggingOut
|
return .loggingOut
|
||||||
case (.pinEntered, .createPIN(let replacingExitingPIN)):
|
case (.pinEntered, .createPIN(let replacingExitingPIN)):
|
||||||
if presentingFlow == .authentication {
|
if presentingFlow == .onboarding {
|
||||||
return appLockService.biometryType != .none ? .biometricsPrompt : .complete
|
return appLockService.biometryType != .none ? .biometricsPrompt : .complete
|
||||||
} else if !replacingExitingPIN {
|
} else if !replacingExitingPIN {
|
||||||
return appLockService.biometricUnlockEnabled || appLockService.biometryType == .none ? .settings : .biometricsPrompt
|
return appLockService.biometricUnlockEnabled || appLockService.biometryType == .none ? .settings : .biometricsPrompt
|
||||||
@@ -182,7 +182,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private func showCreatePIN() {
|
private func showCreatePIN() {
|
||||||
// Despite appLockService.isMandatory existing, we don't use that here,
|
// Despite appLockService.isMandatory existing, we don't use that here,
|
||||||
// to allow for cancellation when changing the PIN code within settings.
|
// 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,
|
let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .create,
|
||||||
isMandatory: isMandatory,
|
isMandatory: isMandatory,
|
||||||
@@ -200,8 +200,12 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if presentingFlow == .authentication {
|
if presentingFlow == .onboarding {
|
||||||
navigationStackCoordinator.push(coordinator)
|
if navigationStackCoordinator.rootCoordinator == nil {
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
} else {
|
||||||
|
navigationStackCoordinator.push(coordinator)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
modalNavigationStackCoordinator.setRootCoordinator(coordinator)
|
modalNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
navigationStackCoordinator.setSheetCoordinator(modalNavigationStackCoordinator)
|
navigationStackCoordinator.setSheetCoordinator(modalNavigationStackCoordinator)
|
||||||
@@ -219,7 +223,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if presentingFlow == .authentication {
|
if presentingFlow == .onboarding {
|
||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
} else {
|
} else {
|
||||||
modalNavigationStackCoordinator.push(coordinator)
|
modalNavigationStackCoordinator.push(coordinator)
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
|
private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
|
||||||
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .authentication,
|
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .onboarding,
|
||||||
appLockService: appLockService,
|
appLockService: appLockService,
|
||||||
navigationStackCoordinator: navigationStackCoordinator)
|
navigationStackCoordinator: navigationStackCoordinator)
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Enforce mandatory app lock outside of the authentication flow.
|
||||||
Reference in New Issue
Block a user