mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Onboarding flow coordinator and FTUE changes (#2578)
Fixes #2595, fixes #2594, fixes #2593, fixes #2592, fixes #2591
This commit is contained in:
@@ -83,7 +83,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
MXLog.info("\(appName) \(appVersion) (\(appBuild))")
|
||||
|
||||
if ProcessInfo.processInfo.environment["RESET_APP_SETTINGS"].map(Bool.init) == true {
|
||||
AppSettings.reset()
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
self.appDelegate = appDelegate
|
||||
@@ -154,11 +154,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
return
|
||||
}
|
||||
|
||||
if appSettings.appLockIsMandatory, !appLockFlowCoordinator.appLockService.isEnabled {
|
||||
stateMachine.processEvent(.startWithAppLockSetup)
|
||||
} else {
|
||||
stateMachine.processEvent(.startWithExistingSession)
|
||||
}
|
||||
stateMachine.processEvent(.startWithExistingSession)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
@@ -319,13 +315,21 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
appSettings.migratedAccounts[userID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if oldVersion < Version(1, 6, 0) {
|
||||
MXLog.info("Migrating to v1.6.0, marking identity confirmation onboarding as ran.")
|
||||
if !userSessionStore.userIDs.isEmpty {
|
||||
appSettings.hasRunIdentityConfirmationOnboarding = true
|
||||
appSettings.hasRunNotificationPermissionsOnboarding = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the keychain, app support directory etc ready for a fresh use.
|
||||
/// - Parameter includingSettings: Whether to additionally wipe the user's app settings too.
|
||||
private func wipeUserData(includingSettings: Bool = false) {
|
||||
if includingSettings {
|
||||
AppSettings.reset()
|
||||
AppSettings.resetAllSettings()
|
||||
appLockFlowCoordinator.appLockService.disable()
|
||||
}
|
||||
userSessionStore.reset()
|
||||
@@ -339,20 +343,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
case (.initial, .startWithAuthentication, .signedOut):
|
||||
startAuthentication()
|
||||
case (.signedOut, .createdUserSession, .signedIn):
|
||||
setupUserSession()
|
||||
setupUserSession(isNewLogin: true)
|
||||
case (.initial, .startWithExistingSession, .restoringSession):
|
||||
restoreUserSession()
|
||||
case (.restoringSession, .failedRestoringSession, .signedOut):
|
||||
showLoginErrorToast()
|
||||
presentSplashScreen()
|
||||
case (.restoringSession, .createdUserSession, .signedIn):
|
||||
setupUserSession()
|
||||
|
||||
case (.initial, .startWithAppLockSetup, .mandatoryAppLockSetup):
|
||||
startMandatoryAppLockSetup()
|
||||
case (.mandatoryAppLockSetup, .appLockSetupComplete, .restoringSession):
|
||||
restoreUserSession()
|
||||
|
||||
setupUserSession(isNewLogin: false)
|
||||
|
||||
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.
|
||||
@@ -397,7 +396,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
appSettings: appSettings)
|
||||
authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
||||
appLockService: appLockFlowCoordinator.appLockService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appSettings: appSettings,
|
||||
@@ -450,7 +448,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserSession() {
|
||||
private func setupUserSession(isNewLogin: Bool) {
|
||||
guard let userSession else {
|
||||
fatalError("User session not setup")
|
||||
}
|
||||
@@ -462,9 +460,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
|
||||
appSettings: appSettings,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
notificationManager: notificationManager,
|
||||
isNewLogin: isNewLogin)
|
||||
|
||||
userSessionFlowCoordinator.actions
|
||||
userSessionFlowCoordinator.actionsPublisher
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -487,32 +487,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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")
|
||||
@@ -544,6 +519,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
tearDownUserSession()
|
||||
|
||||
AppSettings.resetSessionSpecificSettings()
|
||||
|
||||
// Reset analytics
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
ServiceLocator.shared.analytics.resetConsentState()
|
||||
@@ -591,7 +568,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private func configureNotificationManager() {
|
||||
notificationManager.setUserSession(userSession)
|
||||
notificationManager.requestAuthorization()
|
||||
|
||||
appDelegateObserver = appDelegate.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -689,7 +665,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
// MARK: Toasts and loading indicators
|
||||
|
||||
private static let loadingIndicatorIdentifier = "AppCoordinatorLoading"
|
||||
private static let loadingIndicatorIdentifier = "\(AppCoordinator.self)-Loading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
|
||||
@@ -28,12 +28,7 @@ class AppCoordinatorStateMachine {
|
||||
case softLogout
|
||||
/// 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
|
||||
|
||||
@@ -48,21 +43,13 @@ 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.
|
||||
@@ -92,10 +79,7 @@ class AppCoordinatorStateMachine {
|
||||
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
|
||||
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])
|
||||
|
||||
@@ -24,10 +24,13 @@ final class AppSettings {
|
||||
case seenInvites
|
||||
case appLockNumberOfPINAttempts
|
||||
case appLockNumberOfBiometricAttempts
|
||||
case lastLoginDate
|
||||
case migratedAccounts
|
||||
case timelineStyle
|
||||
|
||||
case analyticsConsentState
|
||||
case hasRunNotificationPermissionsOnboarding
|
||||
case hasRunIdentityConfirmationOnboarding
|
||||
|
||||
case enableNotifications
|
||||
case enableInAppNotifications
|
||||
case pusherProfileTag
|
||||
@@ -59,11 +62,16 @@ final class AppSettings {
|
||||
|
||||
#if IS_MAIN_APP
|
||||
|
||||
static func reset() {
|
||||
static func resetAllSettings() {
|
||||
MXLog.warning("Resetting the AppSettings.")
|
||||
store.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
|
||||
static func resetSessionSpecificSettings() {
|
||||
MXLog.warning("Resetting the user session specific AppSettings.")
|
||||
store.removeObject(forKey: UserDefaultsKeys.hasRunIdentityConfirmationOnboarding.rawValue)
|
||||
}
|
||||
|
||||
static func configureWithSuiteName(_ name: String) {
|
||||
suiteName = name
|
||||
|
||||
@@ -120,7 +128,9 @@ final class AppSettings {
|
||||
let privacyURL: URL = "https://element.io/privacy"
|
||||
/// An email address that should be used for support requests.
|
||||
let supportEmailAddress = "support@element.io"
|
||||
// A URL where users can go read more about the chat backup.
|
||||
/// A URL where users can go read more about encryption in general.
|
||||
let encryptionURL: URL = "https://element.io/help#encryption"
|
||||
/// A URL where users can go read more about the chat backup.
|
||||
let chatBackupDetailsURL: URL = "https://element.io/help#encryption5"
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.appAppearance, defaultValue: .system, storageType: .userDefaults(store))
|
||||
@@ -153,13 +163,6 @@ final class AppSettings {
|
||||
|
||||
return url
|
||||
}()
|
||||
|
||||
/// The date that the call to `/login` completed successfully. This is used to put
|
||||
/// a hard wall on the history of encrypted messages until we have key backup.
|
||||
///
|
||||
/// Not a multi-account aware setting as key backup will come before multi-account.
|
||||
@UserPreference(key: UserDefaultsKeys.lastLoginDate, defaultValue: nil, storageType: .userDefaults(store))
|
||||
var lastLoginDate: Date?
|
||||
|
||||
/// A dictionary of accounts that have performed an initial sync through their proxy.
|
||||
///
|
||||
@@ -211,6 +214,12 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.analyticsConsentState, defaultValue: AnalyticsConsentState.unknown, storageType: .userDefaults(store))
|
||||
var analyticsConsentState
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hasRunNotificationPermissionsOnboarding, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasRunNotificationPermissionsOnboarding
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hasRunIdentityConfirmationOnboarding, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasRunIdentityConfirmationOnboarding
|
||||
|
||||
// MARK: - Home Screen
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hideUnreadMessagesBadge, defaultValue: false, storageType: .userDefaults(store))
|
||||
|
||||
Reference in New Issue
Block a user