Skip to content

Commit

Permalink
Onboarding flow coordinator and FTUE changes (#2578)
Browse files Browse the repository at this point in the history
Fixes #2595, fixes #2594, fixes #2593, fixes #2592, fixes #2591
  • Loading branch information
stefanceriu authored Mar 21, 2024
1 parent 9e6b6ba commit a62c96f
Show file tree
Hide file tree
Showing 176 changed files with 1,886 additions and 912 deletions.
280 changes: 186 additions & 94 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "background-bottom-light.png",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "background-bottom-dark.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Alerts.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
4 changes: 4 additions & 0 deletions ElementX/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@
"screen_edit_profile_error_title" = "Unable to update profile";
"screen_edit_profile_title" = "Edit profile";
"screen_edit_profile_updating_details" = "Updating profile…";
"screen_identity_confirmation_subtitle" = "Verify this device to set up secure messaging.";
"screen_identity_confirmation_title" = "Confirm that it's you";
"screen_identity_confirmed_subtitle" = "Now you can read or send messages securely, and anyone you chat with can also trust this device.";
"screen_identity_confirmed_title" = "Device verified";
"screen_invites_decline_chat_message" = "Are you sure you want to decline the invitation to join %1$@?";
"screen_invites_decline_chat_title" = "Decline invite";
"screen_invites_decline_direct_chat_message" = "Are you sure you want to decline this private chat with %1$@?";
Expand Down
68 changes: 22 additions & 46 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
Expand All @@ -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 }

Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -591,7 +568,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg

private func configureNotificationManager() {
notificationManager.setUserSession(userSession)
notificationManager.requestAuthorization()

appDelegateObserver = appDelegate.callbacks
.receive(on: DispatchQueue.main)
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 3 additions & 19 deletions ElementX/Sources/Application/AppCoordinatorStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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])
Expand Down
29 changes: 19 additions & 10 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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))
Expand Down
Loading

0 comments on commit a62c96f

Please sign in to comment.