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

Onboarding flow coordinator and FTUE changes #2578

Merged
merged 18 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
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
}
}
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
66 changes: 21 additions & 45 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
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
Loading