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

Initial implementation for AppLockSetupFlowCoordinator. #1949

Merged
merged 5 commits into from
Oct 25, 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
80 changes: 40 additions & 40 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"common_server_url" = "Server URL";
"common_settings" = "Settings";
"common_shared_location" = "Shared location";
"common_signing_out" = "Signing out";
"common_starting_chat" = "Starting chat…";
"common_sticker" = "Sticker";
"common_success" = "Success";
Expand Down Expand Up @@ -297,7 +298,6 @@
"screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match";
"screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed";
"screen_app_lock_signout_alert_title" = "You are being signed out";
"screen_app_lock_subtitle" = "You have 3 attempts to unlock";
"screen_bug_report_attach_screenshot" = "Attach screenshot";
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions.";
"screen_bug_report_contact_me_title" = "Contact me";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@
<string>%1$d room changes</string>
</dict>
</dict>
<key>screen_app_lock_subtitle</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@COUNT@</string>
<key>COUNT</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>You have %1$d attempt to unlock</string>
<key>other</key>
<string>You have %1$d attempts to unlock</string>
</dict>
</dict>
<key>screen_app_lock_subtitle_wrong_pin</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Expand Down
37 changes: 26 additions & 11 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
backgroundTaskService: backgroundTaskService)

let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
let appLockNavigationCoordinator = NavigationRootCoordinator()
let appLockFlowUserIndicatorController = UserIndicatorController(rootCoordinator: appLockNavigationCoordinator)
appLockFlowCoordinator = AppLockFlowCoordinator(appLockService: appLockService,
navigationCoordinator: NavigationRootCoordinator())
userIndicatorController: appLockFlowUserIndicatorController,
navigationCoordinator: appLockNavigationCoordinator)

notificationManager = NotificationManager(notificationCenter: UNUserNotificationCenter.current(),
appSettings: appSettings)
Expand Down Expand Up @@ -329,12 +332,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
// 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):
case (_, .signOut(let isSoft, _), .signingOut):
self.logout(isSoft: isSoft)
case (.signingOut, .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: false)
case (.signingOut, .showSoftLogout, .softLogout):
self.presentSplashScreen(isSoftLogout: true)
case (.signingOut(_, let disableAppLock), .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: false, disableAppLock: disableAppLock)
case (.signingOut(_, let disableAppLock), .showSoftLogout, .softLogout):
self.presentSplashScreen(isSoftLogout: true, disableAppLock: disableAppLock)
case (.signedIn, .clearCache, .initial):
self.clearCache()
default:
Expand Down Expand Up @@ -371,7 +374,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
navigationStackCoordinator: authenticationNavigationStackCoordinator,
appSettings: appSettings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appLockService: appLockFlowCoordinator.appLockService)
authenticationCoordinator?.delegate = self

authenticationCoordinator?.start()
Expand Down Expand Up @@ -410,7 +414,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
stateMachine.processEvent(.createdUserSession)
case .clearAllData:
self.softLogoutCoordinator = nil
stateMachine.processEvent(.signOut(isSoft: false))
stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: false))
}
}
.store(in: &cancellables)
Expand Down Expand Up @@ -439,7 +443,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,

switch action {
case .logout:
stateMachine.processEvent(.signOut(isSoft: false))
stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: false))
case .clearCache:
stateMachine.processEvent(.clearCache)
}
Expand Down Expand Up @@ -512,14 +516,23 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
notificationManager.setUserSession(nil)
}

private func presentSplashScreen(isSoftLogout: Bool = false) {
private func presentSplashScreen(isSoftLogout: Bool = false, disableAppLock: Bool = false) {
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())

if isSoftLogout {
startAuthenticationSoftLogout()
} else {
startAuthentication()
}

if disableAppLock {
Task {
// Ensure the navigation stack has settled.
try? await Task.sleep(for: .milliseconds(500))
appLockFlowCoordinator.appLockService.disable()
windowManager.switchToMain()
}
}
}

private func configureNotificationManager() {
Expand Down Expand Up @@ -549,7 +562,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
guard let self else { return }
switch callback {
case .didReceiveAuthError(let isSoftLogout):
stateMachine.processEvent(.signOut(isSoft: isSoftLogout))
stateMachine.processEvent(.signOut(isSoft: isSoftLogout, disableAppLock: false))
default:
break
}
Expand Down Expand Up @@ -583,6 +596,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
windowManager.switchToAlternate()
case .unlockApp:
windowManager.switchToMain()
case .forceLogout:
stateMachine.processEvent(.signOut(isSoft: false, disableAppLock: true))
}
}
.store(in: &cancellables)
Expand Down
13 changes: 7 additions & 6 deletions ElementX/Sources/Application/AppCoordinatorStateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class AppCoordinatorStateMachine {
case signedIn

/// Processing a sign out request
case signingOut(isSoft: Bool)
case signingOut(isSoft: Bool, disableAppLock: Bool)
}

/// Events that can be triggered on the AppCoordinator state machine
Expand All @@ -51,7 +51,7 @@ class AppCoordinatorStateMachine {
case createdUserSession

/// Request sign out.
case signOut(isSoft: Bool)
case signOut(isSoft: Bool, disableAppLock: Bool)
/// Request the soft logout screen.
case showSoftLogout
/// Signing out completed.
Expand Down Expand Up @@ -80,16 +80,17 @@ class AppCoordinatorStateMachine {
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])

stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false) => .signedOut])
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true) => .softLogout])
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])

stateMachine.addRoutes(event: .clearCache, transitions: [.signedIn => .initial])

// Transitions with associated values need to be handled through `addRouteMapping`
stateMachine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.signOut(let isSoft), _):
return .signingOut(isSoft: isSoft)
case (.signOut(let isSoft, let disableAppLock), _):
return .signingOut(isSoft: isSoft, disableAppLock: disableAppLock)
default:
return nil
}
Expand Down
12 changes: 11 additions & 1 deletion ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ final class AppSettings {
private enum UserDefaultsKeys: String {
case lastVersionLaunched
case seenInvites
case hasShownWelcomeScreen
case appLockNumberOfPINAttempts
case appLockNumberOfBiometricAttempts
case lastLoginDate
case migratedAccounts
case timelineStyle
Expand All @@ -38,7 +41,6 @@ final class AppSettings {
case shouldCollapseRoomStateEvents
case userSuggestionsEnabled
case readReceiptsEnabled
case hasShownWelcomeScreen
case swiftUITimelineEnabled
case voiceMessageEnabled
case mentionsEnabled
Expand Down Expand Up @@ -122,10 +124,18 @@ final class AppSettings {

// MARK: - Security

/// The app must be locked with a PIN code as part of the authentication flow.
let appLockIsMandatory = false
/// The amount of time the app can remain in the background for without requesting the PIN/TouchID/FaceID.
let appLockGracePeriod: TimeInterval = 180
/// Any codes that the user isn't allowed to use for their PIN.
let appLockPINCodeBlockList = ["0000", "1234"]
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfPINAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfPINAttempts: Int
/// The number of attempts the user has made to unlock the app with Touch/Face ID (resets when unlocked).
@UserPreference(key: UserDefaultsKeys.appLockNumberOfBiometricAttempts, defaultValue: 0, storageType: .userDefaults(store))
var appLockNumberOfBiometricAttempts: Int

// MARK: - Authentication

Expand Down
18 changes: 16 additions & 2 deletions ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ enum AppLockFlowCoordinatorAction: Equatable {
case lockApp
/// Hide the unlock flow.
case unlockApp
/// Forces a logout of the user.
case forceLogout
}

/// Coordinates the display of any screens shown when the app is locked.
class AppLockFlowCoordinator: CoordinatorProtocol {
let appLockService: AppLockServiceProtocol
let userIndicatorController: UserIndicatorController
let navigationCoordinator: NavigationRootCoordinator

private var cancellables: Set<AnyCancellable> = []
Expand All @@ -36,10 +39,18 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}

init(appLockService: AppLockServiceProtocol, navigationCoordinator: NavigationRootCoordinator) {
init(appLockService: AppLockServiceProtocol, userIndicatorController: UserIndicatorController, navigationCoordinator: NavigationRootCoordinator) {
self.appLockService = appLockService
self.userIndicatorController = userIndicatorController
self.navigationCoordinator = navigationCoordinator

appLockService.disabledPublisher
.sink { [weak self] _ in
// When the service is disabled via a force logout, we need to remove the activity indicator.
self?.userIndicatorController.retractAllIndicators()
}
.store(in: &cancellables)

NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.applicationDidEnterBackground()
Expand All @@ -54,7 +65,7 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
}

func toPresentable() -> AnyView {
AnyView(navigationCoordinator.toPresentable())
AnyView(userIndicatorController.toPresentable())
}

// MARK: - App unlock
Expand Down Expand Up @@ -91,6 +102,9 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
switch action {
case .appUnlocked:
actionsSubject.send(.unlockApp)
case .forceLogout:
userIndicatorController.submitIndicator(UserIndicator(type: .modal, title: L10n.commonSigningOut, persistent: true))
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
Expand Down
Loading