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
@@ -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
@@ -1,340 +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
@MainActor
protocol AuthenticationCoordinatorDelegate: AnyObject {
func authenticationCoordinator(didLoginWithSession userSession: UserSessionProtocol)
}
class AuthenticationCoordinator: CoordinatorProtocol {
private let authenticationService: AuthenticationServiceProxyProtocol
private let appLockService: AppLockServiceProtocol
private let bugReportService: BugReportServiceProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private var cancellables = Set<AnyCancellable>()
private var oidcPresenter: OIDCAuthenticationPresenter?
// periphery: ignore - used to store the coordinator to avoid deallocation
private var appLockFlowCoordinator: AppLockSetupFlowCoordinator?
// periphery:ignore - retaining purpose
private var bugReportFlowCoordinator: BugReportFlowCoordinator?
weak var delegate: AuthenticationCoordinatorDelegate?
init(authenticationService: AuthenticationServiceProxyProtocol,
appLockService: AppLockServiceProtocol,
bugReportService: BugReportServiceProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
self.authenticationService = authenticationService
self.appLockService = appLockService
self.bugReportService = bugReportService
self.navigationStackCoordinator = navigationStackCoordinator
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
}
func start() {
showOnboarding()
}
func stop() {
stopLoading()
}
func handleOIDCRedirectURL(_ url: URL) {
guard let oidcPresenter else {
MXLog.error("Failed to find an OIDC request in progress.")
return
}
oidcPresenter.handleUniversalLinkCallback(url)
}
// MARK: - Private
private func showOnboarding() {
let coordinator = OnboardingScreenCoordinator()
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .login:
Task { await self.startAuthentication() }
case .reportProblem:
showReportProblemScreen()
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(coordinator)
}
private func showReportProblemScreen() {
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .sheet(navigationStackCoordinator),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
bugReportService: bugReportService,
userID: nil,
deviceID: nil))
bugReportFlowCoordinator?.start()
}
private func startAuthentication() async {
startLoading()
switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) {
case .success:
stopLoading()
showServerConfirmationScreen()
case .failure:
stopLoading()
showServerSelectionScreen(isModallyPresented: false)
}
}
private func showServerSelectionScreen(isModallyPresented: Bool) {
let navigationCoordinator = NavigationStackCoordinator()
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
userIndicatorController: userIndicatorController,
isModallyPresented: isModallyPresented)
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .updated:
if isModallyPresented {
navigationStackCoordinator.setSheetCoordinator(nil)
} else {
// We are here because the default server failed to respond.
if authenticationService.homeserver.value.loginMode == .password {
// Add the password login screen directly to the flow, its fine.
showLoginScreen()
} else {
// OIDC is presented from the confirmation screen so replace the
// server selection screen which was inserted to handle the failure.
navigationStackCoordinator.pop()
showServerConfirmationScreen()
}
}
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
if isModallyPresented {
navigationCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(navigationCoordinator)
} else {
navigationStackCoordinator.push(coordinator)
}
}
private func showServerConfirmationScreen() {
let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService,
authenticationFlow: .login)
let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .continue(let window):
if authenticationService.homeserver.value.loginMode == .oidc, let window {
showOIDCAuthentication(presentationAnchor: window)
} else {
showLoginScreen()
}
case .changeServer:
showServerSelectionScreen(isModallyPresented: true)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func showOIDCAuthentication(presentationAnchor: UIWindow) {
startLoading()
Task {
switch await authenticationService.urlForOIDCLogin() {
case .failure(let error):
stopLoading()
handleError(error)
case .success(let oidcData):
stopLoading()
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
oidcRedirectURL: appSettings.oidcRedirectURL,
presentationAnchor: presentationAnchor)
self.oidcPresenter = presenter
switch await presenter.authenticate(using: oidcData) {
case .success(let userSession):
userHasSignedIn(userSession: userSession)
case .failure(let error):
handleError(error)
}
oidcPresenter = nil
}
}
}
private func showLoginScreen() {
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
analytics: analytics,
userIndicatorController: userIndicatorController)
let coordinator = LoginScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .signedIn(let userSession):
userHasSignedIn(userSession: userSession)
case .configuredForOIDC:
// Pop back to the confirmation screen for OIDC login to continue.
navigationStackCoordinator.pop(animated: false)
case .isOnWaitlist(let credentials):
showWaitlistScreen(for: credentials)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func showWaitlistScreen(for credentials: WaitlistScreenCredentials) {
let parameters = WaitlistScreenCoordinatorParameters(credentials: credentials,
authenticationService: authenticationService)
let coordinator = WaitlistScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .signedIn(let userSession):
userHasSignedIn(userSession: userSession)
case .cancel:
navigationStackCoordinator.pop()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func userHasSignedIn(userSession: UserSessionProtocol) {
appSettings.lastLoginDate = .now
if appSettings.appLockIsMandatory, !appLockService.isEnabled {
showAppLockSetupFlow(userSession: userSession)
} else if analytics.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(userSession: userSession)
} else {
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
}
}
private func showAppLockSetupFlow(userSession: UserSessionProtocol) {
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .onboarding,
appLockService: appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .complete:
appLockFlowCoordinator = nil
if analytics.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(userSession: userSession)
} else {
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
}
case .forceLogout:
fatalError("The PIN creation flow should not fail.")
}
}
.store(in: &cancellables)
appLockFlowCoordinator = coordinator
coordinator.start()
}
private func showAnalyticsPrompt(userSession: UserSessionProtocol) {
let coordinator = AnalyticsPromptScreenCoordinator(analytics: analytics, termsURL: appSettings.analyticsConfiguration.termsURL)
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .done:
delegate?.authenticationCoordinator(didLoginWithSession: userSession)
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
private func startLoading() {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
}
private func stopLoading() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
MXLog.warning("Error occurred: \(error)")
switch error {
case .oidcError(.notSupported):
// Temporary alert hijacking the use of .notSupported, can be removed when OIDC support is in the SDK.
userIndicatorController.alertInfo = AlertInfo(id: UUID(),
title: L10n.commonError,
message: L10n.commonServerNotSupported)
case .oidcError(.userCancellation):
// No need to show an error, the user cancelled authentication.
break
default:
userIndicatorController.alertInfo = AlertInfo(id: UUID())
}
}
}
@@ -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 }
}