mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
In preparation of FTUE changes... (#2556)
* Remove the welcome screen * Allow the UserSessionFlowCoordinator to control the whole app hierarchy, not only its splitView * Start using the new verification state listener * Rename Onboarding to AuthenticationStart in preparation for the new OnboardingFlowCoordinator; update snapshots and tests * Make the AuthenticationCoordinator a proper FlowCoordinator * Add some padding around the authentication start screen report a problem button * Bump the RustSDK to v1.1.49 * Only add bottom padding on the authentication start screen report a problem button
This commit is contained in:
@@ -20,7 +20,7 @@ import MatrixRustSDK
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, NotificationManagerDelegate, WindowManagerDelegate {
|
||||
class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDelegate, NotificationManagerDelegate, WindowManagerDelegate {
|
||||
private let stateMachine: AppCoordinatorStateMachine
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
@@ -43,7 +43,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
private var authenticationCoordinator: AuthenticationCoordinator?
|
||||
private var authenticationFlowCoordinator: AuthenticationFlowCoordinator?
|
||||
private let appLockFlowCoordinator: AppLockFlowCoordinator
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator?
|
||||
@@ -189,7 +189,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
if stateMachine.state == .softLogout {
|
||||
softLogoutCoordinator?.handleOIDCRedirectURL(url)
|
||||
} else {
|
||||
authenticationCoordinator?.handleOIDCRedirectURL(url)
|
||||
authenticationFlowCoordinator?.handleOIDCRedirectURL(url)
|
||||
}
|
||||
case .genericCallLink(let url):
|
||||
if let userSessionFlowCoordinator {
|
||||
@@ -209,11 +209,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - AuthenticationCoordinatorDelegate
|
||||
// MARK: - AuthenticationFlowCoordinatorDelegate
|
||||
|
||||
func authenticationCoordinator(didLoginWithSession userSession: UserSessionProtocol) {
|
||||
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
authenticationCoordinator = nil
|
||||
authenticationFlowCoordinator = nil
|
||||
stateMachine.processEvent(.createdUserSession)
|
||||
}
|
||||
|
||||
@@ -393,22 +393,19 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
}
|
||||
|
||||
private func startAuthentication() {
|
||||
let authenticationNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore,
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
appSettings: appSettings)
|
||||
authenticationCoordinator = AuthenticationCoordinator(authenticationService: authenticationService,
|
||||
appLockService: appLockFlowCoordinator.appLockService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
navigationStackCoordinator: authenticationNavigationStackCoordinator,
|
||||
appSettings: appSettings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
authenticationCoordinator?.delegate = self
|
||||
authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
|
||||
appLockService: appLockFlowCoordinator.appLockService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appSettings: appSettings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
authenticationFlowCoordinator?.delegate = self
|
||||
|
||||
authenticationCoordinator?.start()
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(authenticationNavigationStackCoordinator)
|
||||
authenticationFlowCoordinator?.start()
|
||||
}
|
||||
|
||||
private func startAuthenticationSoftLogout() {
|
||||
@@ -458,9 +455,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
fatalError("User session not setup")
|
||||
}
|
||||
|
||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
windowManager: windowManager,
|
||||
appLockService: appLockFlowCoordinator.appLockService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
@@ -486,8 +482,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
userSessionFlowCoordinator.start()
|
||||
|
||||
self.userSessionFlowCoordinator = userSessionFlowCoordinator
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
||||
|
||||
if let storedAppRoute {
|
||||
userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false)
|
||||
|
||||
@@ -22,7 +22,6 @@ final class AppSettings {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
case hasShownWelcomeScreen
|
||||
case appLockNumberOfPINAttempts
|
||||
case appLockNumberOfBiometricAttempts
|
||||
case lastLoginDate
|
||||
@@ -109,9 +108,6 @@ final class AppSettings {
|
||||
/// The task identifier used for background app refresh. Also used in main target's the Info.plist
|
||||
let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh"
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hasShownWelcomeScreen, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasShownWelcomeScreen: Bool
|
||||
|
||||
/// A URL where users can go read more about the app.
|
||||
let websiteURL: URL = "https://element.io"
|
||||
/// A URL that contains the app's logo that may be used when showing content in a web view.
|
||||
|
||||
+24
-15
@@ -18,14 +18,15 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
func authenticationCoordinator(didLoginWithSession userSession: UserSessionProtocol)
|
||||
protocol AuthenticationFlowCoordinatorDelegate: AnyObject {
|
||||
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol)
|
||||
}
|
||||
|
||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let appLockService: AppLockServiceProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let appSettings: AppSettings
|
||||
private let analytics: AnalyticsService
|
||||
@@ -41,30 +42,36 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
// periphery:ignore - retaining purpose
|
||||
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
weak var delegate: AuthenticationFlowCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
navigationStackCoordinator: NavigationStackCoordinator,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
appSettings: AppSettings,
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.appLockService = appLockService
|
||||
self.bugReportService = bugReportService
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
self.appSettings = appSettings
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
}
|
||||
|
||||
func start() {
|
||||
showOnboarding()
|
||||
showStartScreen()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func handleOIDCRedirectURL(_ url: URL) {
|
||||
@@ -78,8 +85,8 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func showOnboarding() {
|
||||
let coordinator = OnboardingScreenCoordinator()
|
||||
private func showStartScreen() {
|
||||
let coordinator = AuthenticationStartScreenCoordinator()
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
@@ -95,6 +102,8 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationStackCoordinator)
|
||||
}
|
||||
|
||||
private func showReportProblemScreen() {
|
||||
@@ -263,7 +272,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
} else if analytics.shouldShowAnalyticsPrompt {
|
||||
showAnalyticsPrompt(userSession: userSession)
|
||||
} else {
|
||||
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +288,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
if analytics.shouldShowAnalyticsPrompt {
|
||||
showAnalyticsPrompt(userSession: userSession)
|
||||
} else {
|
||||
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
case .forceLogout:
|
||||
fatalError("The PIN creation flow should not fail.")
|
||||
@@ -299,7 +308,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .done:
|
||||
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
|
||||
delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -307,7 +316,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
private static let loadingIndicatorIdentifier = "authenticationFlowCoordinatorLoading"
|
||||
|
||||
private func startLoading() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
@@ -131,7 +131,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .blockedUsers:
|
||||
presentBlockedUsersScreen()
|
||||
case .sessionVerification:
|
||||
presentSessionVerificationScreen()
|
||||
Task {
|
||||
await self.presentSessionVerificationScreen()
|
||||
}
|
||||
case .accountSessions:
|
||||
presentAccountSessionsListURL()
|
||||
case .notifications:
|
||||
@@ -211,8 +213,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentSessionVerificationScreen() {
|
||||
guard let sessionVerificationController = parameters.userSession.sessionVerificationController else {
|
||||
private func presentSessionVerificationScreen() async {
|
||||
guard case let .success(sessionVerificationController) = await parameters.userSession.clientProxy.sessionVerificationControllerProxy() else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ enum UserSessionFlowCoordinatorAction {
|
||||
|
||||
class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
private let windowManager: WindowManagerProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
@@ -56,7 +57,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(userSession: UserSessionProtocol,
|
||||
navigationSplitCoordinator: NavigationSplitCoordinator,
|
||||
navigationRootCoordinator: NavigationRootCoordinator,
|
||||
windowManager: WindowManagerProtocol,
|
||||
appLockService: AppLockServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
@@ -65,11 +66,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
analytics: AnalyticsService) {
|
||||
stateMachine = UserSessionFlowCoordinatorStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
self.windowManager = windowManager
|
||||
self.bugReportService = bugReportService
|
||||
self.appSettings = appSettings
|
||||
|
||||
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
|
||||
|
||||
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
|
||||
@@ -139,12 +142,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
func start() {
|
||||
if !appSettings.hasShownWelcomeScreen {
|
||||
stateMachine.processEvent(.startWithWelcomeScreen)
|
||||
} else {
|
||||
// Otherwise go straight to the home screen.
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
stateMachine.processEvent(.start)
|
||||
}
|
||||
|
||||
func stop() { }
|
||||
@@ -218,14 +216,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .roomList):
|
||||
presentHomeScreen()
|
||||
|
||||
case (.initial, .startWithWelcomeScreen, .welcomeScreen):
|
||||
presentHomeScreen()
|
||||
presentWelcomeScreen()
|
||||
case (.roomList, .presentWelcomeScreen, .welcomeScreen):
|
||||
presentWelcomeScreen()
|
||||
case (.welcomeScreen, .dismissedWelcomeScreen, .roomList):
|
||||
break
|
||||
|
||||
case(.roomList, .selectRoom, .roomList):
|
||||
break
|
||||
@@ -238,7 +228,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
presentSessionVerification(animated: animated)
|
||||
Task {
|
||||
await self.presentSessionVerification(animated: animated)
|
||||
}
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList):
|
||||
break
|
||||
|
||||
@@ -341,21 +333,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentWelcomeScreen() {
|
||||
let welcomeScreenCoordinator = WelcomeScreenScreenCoordinator()
|
||||
welcomeScreenCoordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(welcomeScreenCoordinator) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedWelcomeScreen)
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
||||
}
|
||||
|
||||
private func runLogoutFlow() async {
|
||||
@@ -405,8 +384,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
// MARK: Session verification
|
||||
|
||||
private func presentSessionVerification(animated: Bool) {
|
||||
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
||||
private func presentSessionVerification(animated: Bool) async {
|
||||
guard case let .success(sessionVerificationController) = await userSession.clientProxy.sessionVerificationControllerProxy() else {
|
||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,7 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
enum State: StateType {
|
||||
/// The initial state, used before the coordinator starts
|
||||
case initial
|
||||
|
||||
/// Showing the welcome screen.
|
||||
case welcomeScreen
|
||||
|
||||
|
||||
/// Showing the home screen. The `selectedRoomID` represents the timeline shown on the detail panel (if any)
|
||||
case roomList(selectedRoomID: String?)
|
||||
|
||||
@@ -56,15 +53,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
enum Event: EventType {
|
||||
/// Start the user session flows.
|
||||
case start
|
||||
/// Starts the user session flows with the welcome screen.
|
||||
/// **Note:** This is event is only for users who used the app before v1.1.8.
|
||||
/// It can be removed once the older TestFlight builds have expired.
|
||||
case startWithWelcomeScreen
|
||||
|
||||
/// Request presentation of the welcome screen.
|
||||
case presentWelcomeScreen
|
||||
/// The welcome screen has been dismissed.
|
||||
case dismissedWelcomeScreen
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomID:the room identifier
|
||||
@@ -116,8 +104,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomID: nil)])
|
||||
stateMachine.addRoutes(event: .startWithWelcomeScreen, transitions: [.initial => .welcomeScreen])
|
||||
stateMachine.addRoutes(event: .dismissedWelcomeScreen, transitions: [.welcomeScreen => .roomList(selectedRoomID: nil)])
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (fromState, event) {
|
||||
@@ -157,9 +143,6 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
case (.invitesScreen(let selectedRoomID), .dismissedInvitesScreen):
|
||||
return .roomList(selectedRoomID: selectedRoomID)
|
||||
|
||||
case (.roomList, .presentWelcomeScreen):
|
||||
return .welcomeScreen
|
||||
|
||||
case (.roomList(let selectedRoomID), .showLogoutConfirmationScreen):
|
||||
return .logoutConfirmationScreen(selectedRoomID: selectedRoomID)
|
||||
|
||||
@@ -38,6 +38,7 @@ extension ClientProxyMock {
|
||||
|
||||
actionsPublisher = PassthroughSubject<ClientProxyAction, Never>().eraseToAnyPublisher()
|
||||
loadingStatePublisher = CurrentValuePublisher<ClientProxyLoadingState, Never>(.notLoading)
|
||||
verificationStatePublisher = CurrentValuePublisher<SessionVerificationState, Never>(.unknown)
|
||||
|
||||
userAvatarURLPublisher = CurrentValueSubject<URL?, Never>(nil).asCurrentValuePublisher()
|
||||
|
||||
|
||||
@@ -727,6 +727,11 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
set(value) { underlyingLoadingStatePublisher = value }
|
||||
}
|
||||
var underlyingLoadingStatePublisher: CurrentValuePublisher<ClientProxyLoadingState, Never>!
|
||||
var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> {
|
||||
get { return underlyingVerificationStatePublisher }
|
||||
set(value) { underlyingVerificationStatePublisher = value }
|
||||
}
|
||||
var underlyingVerificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never>!
|
||||
var userID: String {
|
||||
get { return underlyingUserID }
|
||||
set(value) { underlyingUserID = value }
|
||||
|
||||
@@ -27,7 +27,7 @@ enum A11yIdentifiers {
|
||||
static let changeServerScreen = ChangeServer()
|
||||
static let homeScreen = HomeScreen()
|
||||
static let loginScreen = LoginScreen()
|
||||
static let onboardingScreen = OnboardingScreen()
|
||||
static let authenticationStartScreen = AuthenticationStartScreen()
|
||||
static let reportContent = ReportContent()
|
||||
static let roomScreen = RoomScreen()
|
||||
static let roomDetailsScreen = RoomDetailsScreen()
|
||||
@@ -42,7 +42,6 @@ enum A11yIdentifiers {
|
||||
static let createRoomScreen = CreateRoomScreen()
|
||||
static let invitesScreen = InvitesScreen()
|
||||
static let inviteUsersScreen = InviteUsersScreen()
|
||||
static let welcomeScreen = WelcomeScreen()
|
||||
static let migrationScreen = MigrationScreen()
|
||||
static let notificationSettingsScreen = NotificationSettingsScreen()
|
||||
static let notificationSettingsEditScreen = NotificationSettingsEditScreen()
|
||||
@@ -124,9 +123,9 @@ enum A11yIdentifiers {
|
||||
let unsupportedServer = "login-unsupported_server"
|
||||
}
|
||||
|
||||
struct OnboardingScreen {
|
||||
let signIn = "onboarding-sign_in"
|
||||
let hidden = "onboarding-hidden"
|
||||
struct AuthenticationStartScreen {
|
||||
let signIn = "authentication_start-sign_in"
|
||||
let hidden = "authentication_start-hidden"
|
||||
}
|
||||
|
||||
struct ReportContent {
|
||||
@@ -260,10 +259,6 @@ enum A11yIdentifiers {
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeScreen {
|
||||
let letsGo = "welcome_screen-lets_go"
|
||||
}
|
||||
|
||||
struct MigrationScreen {
|
||||
let message = "migration_screen-message"
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct AnalyticsPromptScreen: View {
|
||||
@ObservedObject var context: AnalyticsPromptScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.onboardingBreakerScreenTopPadding) {
|
||||
FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding) {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
buttons
|
||||
|
||||
@@ -28,9 +28,7 @@ enum UIConstants {
|
||||
static let actionButtonBottomPadding: CGFloat = 24
|
||||
|
||||
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
|
||||
static let onboardingBreakerScreenTopPadding: CGFloat = 80
|
||||
|
||||
static let welcomeScreenTopPadding: CGFloat = 122
|
||||
static let startScreenBreakerScreenTopPadding: CGFloat = 80
|
||||
|
||||
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
|
||||
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
|
||||
|
||||
@@ -27,13 +27,13 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
private var contentTopPadding: CGFloat {
|
||||
if verticalSizeClass == .compact {
|
||||
// Reduced value for iPhones in landscape.
|
||||
return UIConstants.onboardingBreakerScreenTopPadding
|
||||
return UIConstants.startScreenBreakerScreenTopPadding
|
||||
} else if horizontalSizeClass == .compact {
|
||||
// The default value for portrait iPhones.
|
||||
return 2 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
return 2 * UIConstants.startScreenBreakerScreenTopPadding
|
||||
} else {
|
||||
// Larger on iPad specifically for 11" in Landscape.
|
||||
return 2.7 * UIConstants.onboardingBreakerScreenTopPadding
|
||||
return 2.7 * UIConstants.startScreenBreakerScreenTopPadding
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
@ViewBuilder var bottomContent: () -> BottomContent
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.onboardingBreakerScreenTopPadding, horizontalPadding: 0) {
|
||||
FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding, horizontalPadding: 0) {
|
||||
content()
|
||||
.padding(.top, contentTopPadding)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
@@ -68,7 +68,7 @@ struct WaitingDialog<Content: View, BottomContent: View>: View {
|
||||
GeometryReader { geometry in
|
||||
VStack(spacing: -5) {
|
||||
Color.white
|
||||
.frame(maxHeight: UIConstants.onboardingBreakerScreenTopPadding)
|
||||
.frame(maxHeight: UIConstants.startScreenBreakerScreenTopPadding)
|
||||
|
||||
Image(asset: Asset.Images.waitingGradient)
|
||||
.resizable()
|
||||
|
||||
+6
-6
@@ -17,17 +17,17 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
final class OnboardingScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: OnboardingScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<OnboardingScreenCoordinatorAction, Never> = .init()
|
||||
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: AuthenticationStartScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<OnboardingScreenCoordinatorAction, Never> {
|
||||
var actions: AnyPublisher<AuthenticationStartScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
viewModel = OnboardingScreenViewModel()
|
||||
viewModel = AuthenticationStartScreenViewModel()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -48,6 +48,6 @@ final class OnboardingScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(OnboardingScreen(context: viewModel.context))
|
||||
AnyView(AuthenticationStartScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -18,19 +18,19 @@ import SwiftUI
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum OnboardingScreenCoordinatorAction {
|
||||
enum AuthenticationStartScreenCoordinatorAction {
|
||||
case login
|
||||
case reportProblem
|
||||
}
|
||||
|
||||
enum OnboardingScreenViewModelAction {
|
||||
enum AuthenticationStartScreenViewModelAction {
|
||||
case login
|
||||
case reportProblem
|
||||
}
|
||||
|
||||
struct OnboardingScreenViewState: BindableState { }
|
||||
struct AuthenticationStartScreenViewState: BindableState { }
|
||||
|
||||
enum OnboardingScreenViewAction {
|
||||
enum AuthenticationStartScreenViewAction {
|
||||
case login
|
||||
case reportProblem
|
||||
}
|
||||
+6
-6
@@ -17,20 +17,20 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias OnboardingScreenViewModelType = StateStoreViewModel<OnboardingScreenViewState, OnboardingScreenViewAction>
|
||||
typealias AuthenticationStartScreenViewModelType = StateStoreViewModel<AuthenticationStartScreenViewState, AuthenticationStartScreenViewAction>
|
||||
|
||||
class OnboardingScreenViewModel: OnboardingScreenViewModelType, OnboardingScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<OnboardingScreenViewModelAction, Never> = .init()
|
||||
class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType, AuthenticationStartScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<AuthenticationStartScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<OnboardingScreenViewModelAction, Never> {
|
||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: OnboardingScreenViewState())
|
||||
super.init(initialViewState: AuthenticationStartScreenViewState())
|
||||
}
|
||||
|
||||
override func process(viewAction: OnboardingScreenViewAction) {
|
||||
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .login:
|
||||
actionsSubject.send(.login)
|
||||
+3
-3
@@ -17,7 +17,7 @@
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol OnboardingScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<OnboardingScreenViewModelAction, Never> { get }
|
||||
var context: OnboardingScreenViewModelType.Context { get }
|
||||
protocol AuthenticationStartScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> { get }
|
||||
var context: AuthenticationStartScreenViewModelType.Context { get }
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The app's logo styled to fit on various launch pages.
|
||||
struct OnboardingLogo: View {
|
||||
struct AuthenticationStartLogo: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
/// Set to `true` when using on top of `Asset.Images.launchBackground`
|
||||
+10
-9
@@ -17,10 +17,10 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The screen shown at the beginning of the onboarding flow.
|
||||
struct OnboardingScreen: View {
|
||||
struct AuthenticationStartScreen: View {
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
@ObservedObject var context: OnboardingScreenViewModel.Context
|
||||
@ObservedObject var context: AuthenticationStartScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
@@ -30,7 +30,7 @@ struct OnboardingScreen: View {
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.onboardingScreen.hidden)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden)
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
@@ -49,13 +49,14 @@ struct OnboardingScreen: View {
|
||||
Text(L10n.commonReportAProblem)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.frame(width: geometry.size.width)
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.background {
|
||||
OnboardingScreenBackgroundImage()
|
||||
AuthenticationStartScreenBackgroundImage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ struct OnboardingScreen: View {
|
||||
if verticalSizeClass == .regular {
|
||||
Spacer()
|
||||
|
||||
OnboardingLogo(isOnGradient: true)
|
||||
AuthenticationStartLogo(isOnGradient: true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -98,7 +99,7 @@ struct OnboardingScreen: View {
|
||||
Text(L10n.actionContinue)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.onboardingScreen.signIn)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
|
||||
}
|
||||
.padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
|
||||
.readableFrame()
|
||||
@@ -107,10 +108,10 @@ struct OnboardingScreen: View {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct OnboardingScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = OnboardingScreenViewModel()
|
||||
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = AuthenticationStartScreenViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
OnboardingScreen(context: viewModel.context)
|
||||
AuthenticationStartScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The background gradient shown on the launch, splash and onboarding screens.
|
||||
struct OnboardingScreenBackgroundImage: View {
|
||||
struct AuthenticationStartScreenBackgroundImage: View {
|
||||
var body: some View {
|
||||
Image(asset: Asset.Images.launchBackground)
|
||||
.resizable()
|
||||
@@ -75,11 +75,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
if state.securityBannerMode != .dismissed {
|
||||
state.securityBannerMode = .sessionVerification
|
||||
}
|
||||
case (.unverifiedLastSession, .incomplete):
|
||||
state.requiresExtraAccountSetup = true
|
||||
if state.securityBannerMode != .dismissed {
|
||||
state.securityBannerMode = .sessionVerification
|
||||
}
|
||||
case (.verified, .disabled):
|
||||
state.requiresExtraAccountSetup = true
|
||||
state.securityBannerMode = .none
|
||||
|
||||
@@ -33,11 +33,11 @@ struct PlaceholderScreen: View {
|
||||
let showsBackgroundGradient: Bool
|
||||
|
||||
var body: some View {
|
||||
OnboardingLogo(isOnGradient: showsBackgroundGradient)
|
||||
AuthenticationStartLogo(isOnGradient: showsBackgroundGradient)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
if showsBackgroundGradient {
|
||||
OnboardingScreenBackgroundImage()
|
||||
AuthenticationStartScreenBackgroundImage()
|
||||
}
|
||||
}
|
||||
.background()
|
||||
|
||||
+2
-2
@@ -100,9 +100,9 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
|
||||
showSavingIndicator()
|
||||
|
||||
switch await roomProxy.resetPowerLevels() {
|
||||
case .success(let success):
|
||||
case .success:
|
||||
showSuccessIndicator()
|
||||
case .failure(let failure):
|
||||
case .failure:
|
||||
state.bindings.alertInfo = AlertInfo(id: .error)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,6 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
case (.unverified, _):
|
||||
state.showSecuritySectionBadge = true
|
||||
state.securitySectionMode = .sessionVerification
|
||||
case (.unverifiedLastSession, .incomplete):
|
||||
state.showSecuritySectionBadge = true
|
||||
state.securitySectionMode = .sessionVerification
|
||||
case (.verified, .disabled):
|
||||
state.showSecuritySectionBadge = true
|
||||
state.securitySectionMode = .secureBackup
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WelcomeScreen: View {
|
||||
@ObservedObject var context: WelcomeScreenScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: UIConstants.welcomeScreenTopPadding) {
|
||||
mainContent
|
||||
} bottomContent: {
|
||||
button
|
||||
}
|
||||
.background(OnboardingScreenBackgroundImage())
|
||||
.environment(\.backgroundStyle, AnyShapeStyle(Color.clear))
|
||||
.onAppear {
|
||||
context.send(viewAction: .appeared)
|
||||
}
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(spacing: 80) {
|
||||
header
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 32) {
|
||||
OnboardingLogo(isOnGradient: true)
|
||||
.scaleEffect(x: 0.75, y: 0.75)
|
||||
.padding(.vertical, -20)
|
||||
|
||||
Text(L10n.screenWelcomeTitle(InfoPlistReader.main.bundleDisplayName))
|
||||
.font(Font.compound.headingLGBold)
|
||||
.foregroundColor(Color.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
RoundedLabelItem(title: L10n.screenWelcomeBullet2, listPosition: .top) {
|
||||
Image(systemName: "lock")
|
||||
.foregroundColor(.compound.iconSecondaryAlpha)
|
||||
}
|
||||
RoundedLabelItem(title: L10n.screenWelcomeBullet3, listPosition: .bottom) {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.padding(.horizontal, -3)
|
||||
.foregroundColor(.compound.iconSecondaryAlpha)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity)
|
||||
.environment(\.backgroundStyle, AnyShapeStyle(.compound.bgCanvasDefaultLevel1))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var button: some View {
|
||||
Button {
|
||||
context.send(viewAction: .doneTapped)
|
||||
} label: {
|
||||
Text(L10n.screenWelcomeButton)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.welcomeScreen.letsGo)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct WelcomeScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = WelcomeScreenScreenViewModel()
|
||||
|
||||
static var previews: some View {
|
||||
WelcomeScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
enum WelcomeScreenScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class WelcomeScreenScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: WelcomeScreenScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<WelcomeScreenScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<WelcomeScreenScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
viewModel = WelcomeScreenScreenViewModel()
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(WelcomeScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WelcomeScreenScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct WelcomeScreenScreenViewState: BindableState { }
|
||||
|
||||
enum WelcomeScreenScreenViewAction {
|
||||
case doneTapped
|
||||
case appeared
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias WelcomeScreenScreenViewModelType = StateStoreViewModel<WelcomeScreenScreenViewState, WelcomeScreenScreenViewAction>
|
||||
|
||||
class WelcomeScreenScreenViewModel: WelcomeScreenScreenViewModelType, WelcomeScreenScreenViewModelProtocol {
|
||||
let appSettings: AppSettings
|
||||
private var actionsSubject: PassthroughSubject<WelcomeScreenScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<WelcomeScreenScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(appSettings: AppSettings = ServiceLocator.shared.settings) {
|
||||
self.appSettings = appSettings
|
||||
super.init(initialViewState: WelcomeScreenScreenViewState())
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: WelcomeScreenScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .doneTapped:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .appeared:
|
||||
appSettings.hasShownWelcomeScreen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol WelcomeScreenScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<WelcomeScreenScreenViewModelAction, Never> { get }
|
||||
var context: WelcomeScreenScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -41,6 +41,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var ignoredUsersListenerTaskHandle: TaskHandle?
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var verificationStateListenerTaskHandle: TaskHandle?
|
||||
|
||||
private var delegateHandle: TaskHandle?
|
||||
|
||||
// These following summary providers both operate on the same allRooms() list but
|
||||
@@ -111,6 +114,11 @@ class ClientProxy: ClientProxyProtocol {
|
||||
loadingStateSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private let verificationStateSubject = CurrentValueSubject<SessionVerificationState, Never>(.unknown)
|
||||
var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> {
|
||||
verificationStateSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
init(client: ClientProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol,
|
||||
appSettings: AppSettings,
|
||||
@@ -151,6 +159,25 @@ class ClientProxy: ClientProxyProtocol {
|
||||
ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: IgnoredUsersListenerProxy { [weak self] ignoredUsers in
|
||||
self?.ignoredUsersSubject.send(ignoredUsers)
|
||||
})
|
||||
|
||||
updateVerificationState(client.encryption().verificationState())
|
||||
|
||||
verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: VerificationStateListenerProxy { [weak self] verificationState in
|
||||
self?.updateVerificationState(verificationState)
|
||||
})
|
||||
}
|
||||
|
||||
private func updateVerificationState(_ verificationState: VerificationState) {
|
||||
let verificationState: SessionVerificationState = switch verificationState {
|
||||
case .unknown:
|
||||
.unknown
|
||||
case .unverified:
|
||||
.unverified
|
||||
case .verified:
|
||||
.verified
|
||||
}
|
||||
|
||||
verificationStateSubject.send(verificationState)
|
||||
}
|
||||
|
||||
var userID: String {
|
||||
@@ -745,6 +772,18 @@ private class RoomListServiceSyncIndicatorListenerProxy: RoomListServiceSyncIndi
|
||||
}
|
||||
}
|
||||
|
||||
private class VerificationStateListenerProxy: VerificationStateListener {
|
||||
private let onUpdateClosure: (VerificationState) -> Void
|
||||
|
||||
init(onUpdateClosure: @escaping (VerificationState) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(status: VerificationState) {
|
||||
onUpdateClosure(status)
|
||||
}
|
||||
}
|
||||
|
||||
private class ClientDelegateWrapper: ClientDelegate {
|
||||
private let authErrorCallback: (Bool) -> Void
|
||||
|
||||
|
||||
@@ -71,12 +71,20 @@ struct PusherConfiguration {
|
||||
let lang: String
|
||||
}
|
||||
|
||||
enum SessionVerificationState {
|
||||
case unknown
|
||||
case verified
|
||||
case unverified
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
||||
|
||||
var loadingStatePublisher: CurrentValuePublisher<ClientProxyLoadingState, Never> { get }
|
||||
|
||||
var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> { get }
|
||||
|
||||
var userID: String { get }
|
||||
|
||||
var deviceID: String? { get }
|
||||
|
||||
@@ -22,8 +22,6 @@ class UserSession: UserSessionProtocol {
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var retrieveSessionVerificationControllerTask: Task<Void, Never>?
|
||||
|
||||
private var authErrorCancellable: AnyCancellable?
|
||||
|
||||
var userID: String { clientProxy.userID }
|
||||
@@ -36,20 +34,6 @@ class UserSession: UserSessionProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
|
||||
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? {
|
||||
didSet {
|
||||
sessionVerificationController?.callbacks.sink { [weak self] callback in
|
||||
switch callback {
|
||||
case .finished:
|
||||
self?.sessionVerificationStateSubject.send(.verified)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
let sessionSecurityStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .unknown, recoveryState: .unknown))
|
||||
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> {
|
||||
sessionSecurityStateSubject.asCurrentValuePublisher()
|
||||
@@ -60,15 +44,6 @@ class UserSession: UserSessionProtocol {
|
||||
self.mediaProvider = mediaProvider
|
||||
self.voiceMessageMediaManager = voiceMessageMediaManager
|
||||
|
||||
clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
if callback.isSyncUpdate {
|
||||
self?.checkSessionVerificationState()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
authErrorCancellable = clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
@@ -82,7 +57,7 @@ class UserSession: UserSessionProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
Publishers.CombineLatest(sessionVerificationStateSubject, clientProxy.secureBackupController.recoveryState)
|
||||
Publishers.CombineLatest(clientProxy.verificationStatePublisher, clientProxy.secureBackupController.recoveryState)
|
||||
.map {
|
||||
MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)")
|
||||
return SessionSecurityState(verificationState: $0, recoveryState: $1)
|
||||
@@ -94,62 +69,4 @@ class UserSession: UserSessionProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func checkSessionVerificationState() {
|
||||
guard retrieveSessionVerificationControllerTask == nil else {
|
||||
MXLog.info("Session verification state check already in progress")
|
||||
return
|
||||
}
|
||||
|
||||
guard sessionVerificationController == nil else {
|
||||
Task {
|
||||
await updateSessionVerificationState()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.info("Retrieving session verification controller")
|
||||
|
||||
retrieveSessionVerificationControllerTask = Task {
|
||||
switch await clientProxy.sessionVerificationControllerProxy() {
|
||||
case .success(let sessionVerificationController):
|
||||
self.sessionVerificationController = sessionVerificationController
|
||||
await updateSessionVerificationState()
|
||||
|
||||
retrieveSessionVerificationControllerTask = nil
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSessionVerificationState() async {
|
||||
guard let sessionVerificationController else {
|
||||
fatalError("This point should never be reached")
|
||||
}
|
||||
|
||||
MXLog.info("Checking session verification state")
|
||||
|
||||
guard case let .success(isVerified) = await sessionVerificationController.isVerified() else {
|
||||
MXLog.error("Failed checking verification state. Will retry on the next sync update.")
|
||||
return
|
||||
}
|
||||
|
||||
if isVerified {
|
||||
sessionVerificationStateSubject.send(.verified)
|
||||
} else {
|
||||
guard case let .success(isLastDevice) = await clientProxy.isOnlyDeviceLeft() else {
|
||||
MXLog.error("Failed checking isLastDevice. Will retry on the next sync update.")
|
||||
return
|
||||
}
|
||||
|
||||
if isLastDevice {
|
||||
sessionVerificationStateSubject.send(.unverifiedLastSession)
|
||||
} else {
|
||||
sessionVerificationStateSubject.send(.unverified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,6 @@ enum UserSessionCallback {
|
||||
case didReceiveAuthError(isSoftLogout: Bool)
|
||||
}
|
||||
|
||||
enum SessionVerificationState {
|
||||
case unknown
|
||||
case verified
|
||||
case unverified
|
||||
case unverifiedLastSession
|
||||
}
|
||||
|
||||
struct SessionSecurityState: Equatable {
|
||||
let verificationState: SessionVerificationState
|
||||
let recoveryState: SecureBackupRecoveryState
|
||||
@@ -43,7 +36,6 @@ protocol UserSessionProtocol {
|
||||
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
|
||||
|
||||
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> { get }
|
||||
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
|
||||
|
||||
var callbacks: PassthroughSubject<UserSessionCallback, Never> { get }
|
||||
}
|
||||
|
||||
@@ -53,8 +53,12 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
|
||||
func start() {
|
||||
guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") }
|
||||
|
||||
let mockScreen = MockScreen(id: screenID, windowManager: windowManager)
|
||||
navigationRootCoordinator.setRootCoordinator(mockScreen.coordinator)
|
||||
let mockScreen = MockScreen(id: screenID, windowManager: windowManager, navigationRootCoordinator: navigationRootCoordinator)
|
||||
|
||||
if let coordinator = mockScreen.coordinator {
|
||||
navigationRootCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
self.mockScreen = mockScreen
|
||||
}
|
||||
|
||||
@@ -71,8 +75,16 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
|
||||
|
||||
// Set up the alternate window for the App Lock flow coordinator tests.
|
||||
guard let screenID = ProcessInfo.testScreenID, screenID == .appLockFlow || screenID == .appLockFlowDisabled else { return }
|
||||
let screen = MockScreen(id: screenID == .appLockFlow ? .appLockFlowAlternateWindow : .appLockFlowDisabledAlternateWindow, windowManager: windowManager)
|
||||
windowManager.alternateWindow.rootViewController = UIHostingController(rootView: screen.coordinator.toPresentable().statusBarHidden())
|
||||
let screen = MockScreen(id: screenID == .appLockFlow ? .appLockFlowAlternateWindow : .appLockFlowDisabledAlternateWindow,
|
||||
windowManager: windowManager,
|
||||
navigationRootCoordinator: navigationRootCoordinator)
|
||||
|
||||
guard let coordinator = screen.coordinator else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
windowManager.alternateWindow.rootViewController = UIHostingController(rootView: coordinator.toPresentable().statusBarHidden())
|
||||
|
||||
alternateWindowMockScreen = screen
|
||||
}
|
||||
}
|
||||
@@ -81,16 +93,20 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
|
||||
class MockScreen: Identifiable {
|
||||
let id: UITestsScreenIdentifier
|
||||
let windowManager: WindowManagerProtocol
|
||||
let navigationRootCoordinator: NavigationRootCoordinator
|
||||
|
||||
private var retainedState = [Any]()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(id: UITestsScreenIdentifier, windowManager: WindowManagerProtocol) {
|
||||
init(id: UITestsScreenIdentifier,
|
||||
windowManager: WindowManagerProtocol,
|
||||
navigationRootCoordinator: NavigationRootCoordinator) {
|
||||
self.id = id
|
||||
self.windowManager = windowManager
|
||||
self.navigationRootCoordinator = navigationRootCoordinator
|
||||
}
|
||||
|
||||
lazy var coordinator: CoordinatorProtocol = {
|
||||
lazy var coordinator: CoordinatorProtocol? = {
|
||||
switch id {
|
||||
case .login:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
@@ -132,17 +148,16 @@ class MockScreen: Identifiable {
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .authenticationFlow:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
appLockService: AppLockServiceMock(),
|
||||
bugReportService: BugReportServiceMock(),
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
retainedState.append(coordinator)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
appLockService: AppLockServiceMock(),
|
||||
bugReportService: BugReportServiceMock(),
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
flowCoordinator.start()
|
||||
retainedState.append(flowCoordinator)
|
||||
return nil
|
||||
case .softLogout:
|
||||
let credentials = SoftLogoutScreenCredentials(userID: "@mock:matrix.org",
|
||||
homeserverName: "matrix.org",
|
||||
@@ -331,8 +346,8 @@ class MockScreen: Identifiable {
|
||||
notificationSettings: notificationSettings,
|
||||
isModallyPresented: false)
|
||||
return NotificationSettingsScreenCoordinator(parameters: parameters)
|
||||
case .onboarding:
|
||||
return OnboardingScreenCoordinator()
|
||||
case .authenticationStartScreen:
|
||||
return AuthenticationStartScreenCoordinator()
|
||||
case .roomPlainNoAvatar:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(name: "Some room name", avatarURL: nil)),
|
||||
@@ -562,10 +577,9 @@ class MockScreen: Identifiable {
|
||||
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
|
||||
ServiceLocator.shared.settings.migratedAccounts[clientProxy.userID] = true
|
||||
ServiceLocator.shared.settings.hasShownWelcomeScreen = true
|
||||
|
||||
let flowCoordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider(), voiceMessageMediaManager: VoiceMessageMediaManagerMock()),
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
navigationRootCoordinator: navigationRootCoordinator,
|
||||
windowManager: windowManager,
|
||||
appLockService: AppLockService(keychainController: KeychainControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings),
|
||||
@@ -578,7 +592,7 @@ class MockScreen: Identifiable {
|
||||
|
||||
retainedState.append(flowCoordinator)
|
||||
|
||||
return navigationSplitCoordinator
|
||||
return nil
|
||||
case .roomDetailsScreen:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
|
||||
@@ -41,7 +41,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case bugReportWithScreenshot
|
||||
case notificationSettingsScreen
|
||||
case notificationSettingsScreenMismatchConfiguration
|
||||
case onboarding
|
||||
case authenticationStartScreen
|
||||
case roomPlainNoAvatar
|
||||
case roomEncryptedWithAvatar
|
||||
case roomSmallTimeline
|
||||
|
||||
Reference in New Issue
Block a user