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:
Stefan Ceriu
2024-03-13 11:24:48 +02:00
committed by GitHub
parent d7e0bbc537
commit 5944550721
56 changed files with 277 additions and 754 deletions
@@ -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.
@@ -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()
@@ -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))
}
}
@@ -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
}
@@ -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)
@@ -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 }
}
@@ -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`
@@ -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)
}
}
@@ -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()
@@ -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
}
}
}
@@ -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