Move session related responsibilities from ChatsFlowFlowCoordinator to UserSessionFlowCoordinator.

Specifically: onboarding, session verification and logout.
This commit is contained in:
Doug
2025-07-30 15:19:56 +01:00
committed by Doug
parent a18fc8ff5b
commit dac804309e
9 changed files with 342 additions and 217 deletions

View File

@@ -17,6 +17,8 @@ import SwiftUI
let selectedIcon: KeyPath<CompoundIcons, Image>
}
// MARK: Tabs
fileprivate struct TabModule: Identifiable {
let module: NavigationModule
let title: String
@@ -58,8 +60,108 @@ import SwiftUI
}
}
// MARK: Sheets
fileprivate var sheetModule: NavigationModule? {
didSet {
if let oldValue {
logPresentationChange("Remove sheet", oldValue)
oldValue.tearDown()
}
if let sheetModule {
logPresentationChange("Set sheet", sheetModule)
sheetModule.coordinator?.start()
}
}
}
var presentationDetents: Set<PresentationDetent> = []
/// The currently presented sheet coordinator.
var sheetCoordinator: (any CoordinatorProtocol)? {
sheetModule?.coordinator
}
/// Present a sheet on top of the stack. If this NavigationStackCoordinator is embedded within a NavigationSplitCoordinator
/// then the presentation will be proxied to the split
/// - Parameters:
/// - coordinator: the coordinator to display
/// - animated: whether to animate the transition or not. Default is true
/// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise
func setSheetCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) {
guard let coordinator else {
sheetModule = nil
return
}
if sheetModule?.coordinator === coordinator {
fatalError("Cannot use the same coordinator more than once")
}
var transaction = Transaction()
transaction.disablesAnimations = !animated
withTransaction(transaction) {
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
}
// MARK: Full Screen Cover
fileprivate var fullScreenCoverModule: NavigationModule? {
didSet {
if let oldValue {
logPresentationChange("Remove fullscreen cover", oldValue)
oldValue.tearDown()
}
if let fullScreenCoverModule {
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
fullScreenCoverModule.coordinator?.start()
}
}
}
/// The currently presented fullscreen cover coordinator
/// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
fullScreenCoverModule?.coordinator
}
/// Present a fullscreen cover on top of the stack. If this NavigationStackCoordinator is embedded within a NavigationSplitCoordinator
/// then the presentation will be proxied to the split
/// - Parameters:
/// - coordinator: the coordinator to display
/// - animated: whether to animate the transition or not. Default is true
/// - dismissalCallback: called when the fullscreen cover has been dismissed, programatically or otherwise
func setFullScreenCoverCoordinator(_ coordinator: (any CoordinatorProtocol)?, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) {
guard let coordinator else {
fullScreenCoverModule = nil
return
}
if fullScreenCoverModule?.coordinator === coordinator {
fatalError("Cannot use the same coordinator more than once")
}
var transaction = Transaction()
transaction.disablesAnimations = !animated
withTransaction(transaction) {
fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
}
// MARK: - CoordinatorProtocol
/// No idea if this is particuarly needed for the TabView but we do this for the NavigationStackCoordinator and NavigationSplitCoordinator so it
/// doesn't seem to harm to also do it here.
func stop() {
tabModules.forEach { $0.module.tearDown() }
}
func toPresentable() -> AnyView {
AnyView(NavigationTabCoordinatorView(navigationTabCoordinator: self))
}
@@ -99,5 +201,13 @@ private struct NavigationTabCoordinatorView: View {
.id(module.id)
}
}
.sheet(item: $navigationTabCoordinator.sheetModule) { module in
module.coordinator?.toPresentable()
.id(module.id)
}
.fullScreenCover(item: $navigationTabCoordinator.fullScreenCoverModule) { module in
module.coordinator?.toPresentable()
.id(module.id)
}
}
}

View File

@@ -13,8 +13,9 @@ import SwiftUI
enum ChatsFlowCoordinatorAction {
case logout
case sessionVerification(SessionVerificationScreenFlow)
case clearCache
/// Logout without a confirmation. The user forgot their PIN.
/// Logout and disable App Lock without any confirmation. The user forgot their PIN.
case forceLogout
}
@@ -37,8 +38,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
private let settingsFlowCoordinator: SettingsFlowCoordinator
private let onboardingFlowCoordinator: OnboardingFlowCoordinator
// periphery:ignore - retaining purpose
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
@@ -103,16 +102,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: analytics))
onboardingFlowCoordinator = OnboardingFlowCoordinator(userSession: userSession,
appLockService: appLockService,
analyticsService: analytics,
appSettings: appSettings,
notificationManager: notificationManager,
navigationStackCoordinator: detailNavigationStackCoordinator,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
windowManager: appMediator.windowManager,
isNewLogin: isNewLogin)
setupStateMachine()
setupObservers()
@@ -216,15 +205,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
}
}
func attemptStartingOnboarding() {
MXLog.info("Attempting to start onboarding")
if onboardingFlowCoordinator.shouldStart {
clearRoute(animated: false)
onboardingFlowCoordinator.start()
}
}
private func clearPresentedSheets(animated: Bool) async {
if navigationSplitCoordinator.sheetCoordinator == nil {
return
@@ -243,7 +223,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
switch (context.fromState, context.event, context.toState) {
case (.initial, .start, .roomList):
presentHomeScreen()
attemptStartingOnboarding()
case(.roomList(let roomListSelectedRoomID), .selectRoom(let roomID, let via, let entryPoint), .roomList):
if roomListSelectedRoomID == roomID,
!entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading.
@@ -291,11 +270,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
break
case (.roomList, .showLogoutConfirmationScreen, .logoutConfirmationScreen):
presentSecureBackupLogoutConfirmationScreen()
case (.logoutConfirmationScreen, .dismissedLogoutConfirmationScreen, .roomList):
break
case (.roomList, .showRoomDirectorySearchScreen, .roomDirectorySearchScreen):
presentRoomDirectorySearch()
case (.roomDirectorySearchScreen, .dismissedRoomDirectorySearchScreen, .roomList):
@@ -352,19 +326,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
}
private func setupObservers() {
userSession.sessionSecurityStatePublisher
.map(\.verificationState)
.filter { $0 != .unknown }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
attemptStartingOnboarding()
setupSessionVerificationRequestsObserver()
}
.store(in: &cancellables)
settingsFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return }
@@ -374,7 +335,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case .dismissedSettings:
stateMachine.processEvent(.dismissedSettingsScreen)
case .runLogoutFlow:
Task { await self.runLogoutFlow() }
actionsSubject.send(.logout)
case .clearCache:
actionsSubject.send(.clearCache)
case .forceLogout:
@@ -413,17 +374,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
}
}
.store(in: &cancellables)
onboardingFlowCoordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .logout:
logout()
}
}
.store(in: &cancellables)
}
private func processDecryptionError(_ info: UnableToDecryptInfo) {
@@ -455,53 +405,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
wasVisibleToUser: nil)
}
private func setupSessionVerificationRequestsObserver() {
userSession.clientProxy.sessionVerificationController?.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self, case .receivedVerificationRequest(let details) = action else {
return
}
MXLog.info("Received session verification request")
if details.senderProfile.userID == userSession.clientProxy.userID {
presentSessionVerificationScreen(flow: .deviceResponder(requestDetails: details))
} else {
presentSessionVerificationScreen(flow: .userResponder(requestDetails: details))
}
}
.store(in: &cancellables)
}
private func presentSessionVerificationScreen(flow: SessionVerificationScreenFlow) {
guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
flow: flow,
appSettings: appSettings,
mediaProvider: userSession.mediaProvider)
let coordinator = SessionVerificationScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
switch action {
case .done:
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(coordinator)
navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator)
}
private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
bugReportService: bugReportService,
@@ -543,7 +446,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case .presentGlobalSearch:
presentGlobalSearch()
case .logout:
Task { await self.runLogoutFlow() }
actionsSubject.send(.logout)
case .presentDeclineAndBlock(let userID, let roomID):
stateMachine.processEvent(.presentDeclineAndBlockScreen(userID: userID, roomID: roomID))
}
@@ -604,55 +507,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func runLogoutFlow() async {
let secureBackupController = userSession.clientProxy.secureBackupController
guard case let .success(isLastDevice) = await userSession.clientProxy.isOnlyDeviceLeft() else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init())
return
}
guard isLastDevice else {
logout()
return
}
guard secureBackupController.recoveryState.value == .enabled else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutRecoveryDisabledTitle,
message: L10n.screenSignoutRecoveryDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
guard secureBackupController.keyBackupState.value == .enabled else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutKeyBackupDisabledTitle,
message: L10n.screenSignoutKeyBackupDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
presentSecureBackupLogoutConfirmationScreen()
}
private func logout() {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutConfirmationDialogTitle,
message: L10n.screenSignoutConfirmationDialogContent,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
})
}
// MARK: Room Flow
private func startRoomFlow(roomID: String,
@@ -680,7 +534,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
// Here we assume that the app is running and the call state is already up to date
presentCallScreen(roomProxy: roomProxy, notifyOtherParticipants: !roomProxy.infoPublisher.value.hasRoomCall)
case .verifyUser(let userID):
presentSessionVerificationScreen(flow: .userIntiator(userID: userID))
actionsSubject.send(.sessionVerification(.userInitiator(userID: userID)))
case .finished:
stateMachine.processEvent(.deselectRoom)
}
@@ -890,28 +744,6 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentSecureBackupLogoutConfirmationScreen() {
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
appMediator: appMediator))
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
navigationSplitCoordinator.setSheetCoordinator(nil)
case .settings:
settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
case .logout:
actionsSubject.send(.logout)
}
}
.store(in: &cancellables)
navigationSplitCoordinator.setSheetCoordinator(coordinator, animated: true)
}
// MARK: Global search
private func presentGlobalSearch() {

View File

@@ -110,11 +110,6 @@ class ChatsFlowCoordinatorStateMachine {
case showStartChatScreen
/// Start chat has been dismissed
case dismissedStartChatScreen
/// Logout has been requested and this is the last session
case showLogoutConfirmationScreen
/// Logout has been cancelled
case dismissedLogoutConfirmationScreen
/// Request presentation of the room directory search screen.
case showRoomDirectorySearchScreen
@@ -186,11 +181,6 @@ class ChatsFlowCoordinatorStateMachine {
return .startChatScreen(roomListSelectedRoomID: roomListSelectedRoomID)
case (.startChatScreen(let roomListSelectedRoomID), .dismissedStartChatScreen):
return .roomList(roomListSelectedRoomID: roomListSelectedRoomID)
case (.roomList(let roomListSelectedRoomID), .showLogoutConfirmationScreen):
return .logoutConfirmationScreen(roomListSelectedRoomID: roomListSelectedRoomID)
case (.logoutConfirmationScreen(let roomListSelectedRoomID), .dismissedLogoutConfirmationScreen):
return .roomList(roomListSelectedRoomID: roomListSelectedRoomID)
case (.roomList(let roomListSelectedRoomID), .showRoomDirectorySearchScreen):
return .roomDirectorySearchScreen(roomListSelectedRoomID: roomListSelectedRoomID)

View File

@@ -10,6 +10,8 @@ import Foundation
import SwiftState
enum OnboardingFlowCoordinatorAction {
case requestPresentation(animated: Bool)
case dismiss
case logout
}
@@ -19,7 +21,6 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
private let analyticsService: AnalyticsService
private let appSettings: AppSettings
private let notificationManager: NotificationManagerProtocol
private let rootNavigationStackCoordinator: NavigationStackCoordinator
private let userIndicatorController: UserIndicatorControllerProtocol
private let windowManager: WindowManagerProtocol
private let isNewLogin: Bool
@@ -74,8 +75,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
self.windowManager = windowManager
self.isNewLogin = isNewLogin
rootNavigationStackCoordinator = navigationStackCoordinator
self.navigationStackCoordinator = NavigationStackCoordinator()
self.navigationStackCoordinator = navigationStackCoordinator
stateMachine = .init(state: .initial)
@@ -112,7 +112,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
fatalError("This flow coordinator shouldn't have been started")
}
rootNavigationStackCoordinator.setFullScreenCoverCoordinator(navigationStackCoordinator, animated: !isNewLogin)
actionsSubject.send(.requestPresentation(animated: !isNewLogin))
stateMachine.tryEvent(.next)
}
@@ -224,7 +224,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
case (_, _, .notificationPermissions):
presentNotificationPermissionsScreen()
case (_, _, .finished):
rootNavigationStackCoordinator.setFullScreenCoverCoordinator(nil)
actionsSubject.send(.dismiss)
stateMachine.tryState(.initial)
case (.finished, _, .initial):
break

View File

@@ -13,7 +13,7 @@ import SwiftUI
enum UserSessionFlowCoordinatorAction {
case logout
case clearCache
/// Logout without a confirmation. The user forgot their PIN.
/// Logout and disable App Lock without any confirmation. The user forgot their PIN.
case forceLogout
}
@@ -21,7 +21,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let userSession: UserSessionProtocol
private let navigationRootCoordinator: NavigationRootCoordinator
private let navigationTabCoordinator: NavigationTabCoordinator
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let onboardingFlowCoordinator: OnboardingFlowCoordinator
private let onboardingStackCoordinator: NavigationStackCoordinator
private let chatsFlowCoordinator: ChatsFlowCoordinator
private let actionsSubject: PassthroughSubject<UserSessionFlowCoordinatorAction, Never> = .init()
@@ -45,6 +49,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
isNewLogin: Bool) {
self.userSession = userSession
self.navigationRootCoordinator = navigationRootCoordinator
self.appMediator = appMediator
self.appSettings = appSettings
navigationTabCoordinator = NavigationTabCoordinator()
navigationRootCoordinator.setRootCoordinator(navigationTabCoordinator)
@@ -63,28 +69,31 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
notificationManager: notificationManager,
isNewLogin: isNewLogin)
onboardingStackCoordinator = NavigationStackCoordinator()
onboardingFlowCoordinator = OnboardingFlowCoordinator(userSession: userSession,
appLockService: appLockService,
analyticsService: analytics,
appSettings: appSettings,
notificationManager: notificationManager,
navigationStackCoordinator: onboardingStackCoordinator,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
windowManager: appMediator.windowManager,
isNewLogin: isNewLogin)
navigationTabCoordinator.setTabs([
.init(coordinator: chatsSplitCoordinator, title: L10n.screenHomeTabChats, icon: \.chat, selectedIcon: \.chatSolid),
.init(coordinator: BlankFormCoordinator(), title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid)
])
chatsFlowCoordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .logout:
actionsSubject.send(.logout)
case .clearCache:
actionsSubject.send(.clearCache)
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
setupObservers()
}
func start() {
#warning("This flow still needs a state machine.")
chatsFlowCoordinator.start()
attemptStartingOnboarding()
}
func stop() {
@@ -92,6 +101,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
// There aren't any routes that directly target this flow yet, so pass them directly to the
// chats flow coordinator.
#warning("This should switch tabs to make sure the route is visible.")
chatsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
}
@@ -99,8 +111,189 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
chatsFlowCoordinator.clearRoute(animated: animated)
}
#warning("Should this be a publisher instead??")
#warning("This should use a publisher, combining it with the active tab.")
func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool {
chatsFlowCoordinator.isDisplayingRoomScreen(withRoomID: roomID)
}
// MARK: - Private
private func setupObservers() {
chatsFlowCoordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .logout:
Task { await self.runLogoutFlow() }
case .sessionVerification(let flow):
presentSessionVerificationScreen(flow: flow)
case .clearCache:
actionsSubject.send(.clearCache)
case .forceLogout:
actionsSubject.send(.forceLogout)
}
}
.store(in: &cancellables)
userSession.sessionSecurityStatePublisher
.map(\.verificationState)
.filter { $0 != .unknown }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
attemptStartingOnboarding()
setupSessionVerificationRequestsObserver()
}
.store(in: &cancellables)
onboardingFlowCoordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .requestPresentation(let animated):
navigationTabCoordinator.setFullScreenCoverCoordinator(onboardingStackCoordinator, animated: animated)
case .dismiss:
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
case .logout:
logout()
}
}
.store(in: &cancellables)
}
// MARK: - Onboarding
func attemptStartingOnboarding() {
MXLog.info("Attempting to start onboarding")
if onboardingFlowCoordinator.shouldStart {
clearRoute(animated: false)
onboardingFlowCoordinator.start()
}
}
// MARK: - Session Verification
private func setupSessionVerificationRequestsObserver() {
userSession.clientProxy.sessionVerificationController?.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self, case .receivedVerificationRequest(let details) = action else {
return
}
MXLog.info("Received session verification request")
if details.senderProfile.userID == userSession.clientProxy.userID {
presentSessionVerificationScreen(flow: .deviceResponder(requestDetails: details))
} else {
presentSessionVerificationScreen(flow: .userResponder(requestDetails: details))
}
}
.store(in: &cancellables)
}
private func presentSessionVerificationScreen(flow: SessionVerificationScreenFlow) {
guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
flow: flow,
appSettings: appSettings,
mediaProvider: userSession.mediaProvider)
let coordinator = SessionVerificationScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
switch action {
case .done:
self?.navigationTabCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(coordinator)
navigationTabCoordinator.setSheetCoordinator(navigationStackCoordinator)
}
// MARK: - Logout
private func runLogoutFlow() async {
let secureBackupController = userSession.clientProxy.secureBackupController
guard case let .success(isLastDevice) = await userSession.clientProxy.isOnlyDeviceLeft() else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init())
return
}
guard isLastDevice else {
logout()
return
}
guard secureBackupController.recoveryState.value == .enabled else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutRecoveryDisabledTitle,
message: L10n.screenSignoutRecoveryDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
guard secureBackupController.keyBackupState.value == .enabled else {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutKeyBackupDisabledTitle,
message: L10n.screenSignoutKeyBackupDisabledSubtitle,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
}, secondaryButton: .init(title: L10n.commonSettings, role: .cancel) { [weak self] in
self?.chatsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
})
return
}
presentSecureBackupLogoutConfirmationScreen()
}
private func logout() {
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenSignoutConfirmationDialogTitle,
message: L10n.screenSignoutConfirmationDialogContent,
primaryButton: .init(title: L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { [weak self] in
self?.actionsSubject.send(.logout)
})
}
private func presentSecureBackupLogoutConfirmationScreen() {
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
appMediator: appMediator))
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
navigationTabCoordinator.setSheetCoordinator(nil)
case .settings:
chatsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
navigationTabCoordinator.setSheetCoordinator(nil)
case .logout:
actionsSubject.send(.logout)
}
}
.store(in: &cancellables)
navigationTabCoordinator.setSheetCoordinator(coordinator, animated: true)
}
}

View File

@@ -16,12 +16,12 @@ enum SessionVerificationScreenCoordinatorAction {
enum SessionVerificationScreenFlow {
case deviceInitiator
case deviceResponder(requestDetails: SessionVerificationRequestDetails)
case userIntiator(userID: String)
case userInitiator(userID: String)
case userResponder(requestDetails: SessionVerificationRequestDetails)
var isResponder: Bool {
switch self {
case .deviceInitiator, .userIntiator:
case .deviceInitiator, .userInitiator:
false
case .deviceResponder, .userResponder:
true

View File

@@ -36,7 +36,7 @@ struct SessionVerificationScreenViewState: BindableState {
switch flow {
case .deviceInitiator, .deviceResponder:
return (\.devices, .defaultSolid)
case .userIntiator, .userResponder:
case .userInitiator, .userResponder:
return (\.userProfileSolid, .defaultSolid)
}
case .acceptingVerificationRequest:
@@ -74,7 +74,7 @@ struct SessionVerificationScreenViewState: BindableState {
switch flow {
case .deviceInitiator:
return L10n.screenSessionVerificationUseAnotherDeviceTitle
case .userIntiator:
case .userInitiator:
return L10n.screenSessionVerificationUserInitiatorTitle
case .deviceResponder, .userResponder:
return L10n.screenSessionVerificationRequestTitle
@@ -108,7 +108,7 @@ struct SessionVerificationScreenViewState: BindableState {
switch flow {
case .deviceInitiator, .deviceResponder:
return L10n.screenSessionVerificationWaitingOtherDeviceTitle
case .userIntiator, .userResponder:
case .userInitiator, .userResponder:
return L10n.screenSessionVerificationWaitingOtherUserTitle
}
}
@@ -119,7 +119,7 @@ struct SessionVerificationScreenViewState: BindableState {
switch flow {
case .deviceInitiator:
return L10n.screenSessionVerificationUseAnotherDeviceSubtitle
case .userIntiator:
case .userInitiator:
return L10n.screenSessionVerificationUserInitiatorSubtitle
case .deviceResponder:
return L10n.screenSessionVerificationRequestSubtitle
@@ -146,14 +146,14 @@ struct SessionVerificationScreenViewState: BindableState {
switch flow {
case .deviceInitiator, .deviceResponder:
return L10n.screenSessionVerificationCompareEmojisSubtitle
case .userIntiator, .userResponder:
case .userInitiator, .userResponder:
return L10n.screenSessionVerificationCompareEmojisUserSubtitle
}
case .verified:
switch flow {
case .deviceInitiator, .deviceResponder:
return L10n.screenSessionVerificationCompleteSubtitle
case .userIntiator, .userResponder:
case .userInitiator, .userResponder:
return L10n.screenSessionVerificationCompleteUserSubtitle
}

View File

@@ -178,7 +178,7 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
switch flow {
case .deviceInitiator:
return await sessionVerificationControllerProxy.requestDeviceVerification()
case .userIntiator(let userID):
case .userInitiator(let userID):
return await sessionVerificationControllerProxy.requestUserVerification(userID)
default:
fatalError("Incorrect API usage.")

View File

@@ -43,7 +43,7 @@ struct SessionVerificationScreen: View {
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
switch context.viewState.flow {
case .userIntiator, .userResponder:
case .userInitiator, .userResponder:
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
@@ -91,7 +91,7 @@ struct SessionVerificationScreen: View {
SessionVerificationRequestDetailsView(details: details,
isUserVerification: true,
mediaProvider: context.mediaProvider)
case .userIntiator:
case .userInitiator:
Button(L10n.actionLearnMore) {
UIApplication.shared.open(context.viewState.learnMoreURL)
}
@@ -129,7 +129,7 @@ struct SessionVerificationScreen: View {
switch context.viewState.verificationState {
case .initial:
switch context.viewState.flow {
case .deviceInitiator, .userIntiator:
case .deviceInitiator, .userInitiator:
Button(L10n.actionStartVerification) {
context.send(viewAction: .requestVerification)
}
@@ -152,7 +152,7 @@ struct SessionVerificationScreen: View {
}
case .cancelled:
switch context.viewState.flow {
case .deviceInitiator, .userIntiator:
case .deviceInitiator, .userInitiator:
Button(L10n.actionRetry) {
context.send(viewAction: .restart)
}
@@ -214,7 +214,7 @@ struct SessionVerification_Previews: PreviewProvider, TestablePreview {
sessionVerificationScreen(state: .initial, flow: .deviceInitiator)
.previewDisplayName("Initial - Device Initiator")
sessionVerificationScreen(state: .initial, flow: .userIntiator(userID: "@bob:matrix.org"))
sessionVerificationScreen(state: .initial, flow: .userInitiator(userID: "@bob:matrix.org"))
.previewDisplayName("Initial - User Initiator")
let details = SessionVerificationRequestDetails(senderProfile: UserProfileProxy(userID: "@bob:matrix.org",