mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Adopt StateStoreViewModelV2 in the remaining settings screens. (#4158)
* Move the AuthenticationStartScreen into the Authentication directory.
* Commit the updated Sentry license.
No idea why they dropped the 2024 🤷♂️
* Use StateStoreViewModelV2 in BugReportScreen.
* Use StateStoreViewModelV2 in UserDetailsEditScreen.
* Use StateStoreViewModelV2 in NotificationSettingsScreen.
* Use StateStoreViewModelV2 in NotificationSettingsEditScreen.
* Use StateStoreViewModelV2 in LegalInformationScreen.
* Use StateStoreViewModelV2 in LogViewerScreen.
* Use StateStoreViewModelV2 in AnalyticsSettingsScreen.
* Rename AdvancedSettingsScreen directory.
* Use StateStoreViewModelV2 in EncryptionResetScreen.
* Use StateStoreViewModelV2 in EncryptionResetPasswordScreen.
* Use StateStoreViewModelV2 in SecureBackup…Screens.
* Use StateStoreViewModelV2 in LoginScreen.
Seems this one was ignored waiting on the fulfillment transitionValues implementation.
* Use StateStoreViewModelV2 in DeactivateAccountScreen.
* Move DeactivateAccountScreen into the Settings directory.
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct AuthenticationStartScreenParameters {
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
let provisioningParameters: AccountProvisioningParameters?
|
||||
let isBugReportServiceEnabled: Bool
|
||||
let appSettings: AppSettings
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
final class AuthenticationStartScreenCoordinator: CoordinatorProtocol {
|
||||
private var viewModel: AuthenticationStartScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<AuthenticationStartScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var actions: AnyPublisher<AuthenticationStartScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: AuthenticationStartScreenParameters) {
|
||||
viewModel = AuthenticationStartScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
provisioningParameters: parameters.provisioningParameters,
|
||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||
appSettings: parameters.appSettings,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
viewModel.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .loginWithQR:
|
||||
actionsSubject.send(.loginWithQR)
|
||||
case .login:
|
||||
actionsSubject.send(.login)
|
||||
case .register:
|
||||
actionsSubject.send(.register)
|
||||
case .reportProblem:
|
||||
actionsSubject.send(.reportProblem)
|
||||
|
||||
case .loginDirectlyWithOIDC(let data, let window):
|
||||
actionsSubject.send(.loginDirectlyWithOIDC(data: data, window: window))
|
||||
case .loginDirectlyWithPassword(let loginHint):
|
||||
actionsSubject.send(.loginDirectlyWithPassword(loginHint: loginHint))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(AuthenticationStartScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum AuthenticationStartScreenCoordinatorAction {
|
||||
case loginWithQR
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
|
||||
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||
case loginDirectlyWithPassword(loginHint: String?)
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenViewModelAction: Equatable {
|
||||
case loginWithQR
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
|
||||
case loginDirectlyWithOIDC(data: OIDCAuthorizationDataProxy, window: UIWindow)
|
||||
case loginDirectlyWithPassword(loginHint: String?)
|
||||
}
|
||||
|
||||
struct AuthenticationStartScreenViewState: BindableState {
|
||||
/// The presentation anchor used for OIDC authentication.
|
||||
var window: UIWindow?
|
||||
|
||||
let serverName: String?
|
||||
let showCreateAccountButton: Bool
|
||||
let showQRCodeLoginButton: Bool
|
||||
let showReportProblemButton: Bool
|
||||
|
||||
var bindings = AuthenticationStartScreenViewStateBindings()
|
||||
|
||||
var loginButtonTitle: String {
|
||||
if let serverName {
|
||||
L10n.screenOnboardingSignInTo(serverName)
|
||||
} else if showQRCodeLoginButton {
|
||||
L10n.screenOnboardingSignInManually
|
||||
} else {
|
||||
L10n.actionContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationStartScreenViewStateBindings {
|
||||
var alertInfo: AlertInfo<AuthenticationStartScreenAlertType>?
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenAlertType {
|
||||
case genericError
|
||||
}
|
||||
|
||||
enum AuthenticationStartScreenViewAction {
|
||||
/// Updates the window used as the OIDC presentation anchor.
|
||||
case updateWindow(UIWindow)
|
||||
|
||||
case loginWithQR
|
||||
case login
|
||||
case register
|
||||
case reportProblem
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias AuthenticationStartScreenViewModelType = StateStoreViewModelV2<AuthenticationStartScreenViewState, AuthenticationStartScreenViewAction>
|
||||
|
||||
class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType, AuthenticationStartScreenViewModelProtocol {
|
||||
private let authenticationService: AuthenticationServiceProtocol
|
||||
private let provisioningParameters: AccountProvisioningParameters?
|
||||
private let appSettings: AppSettings
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private var actionsSubject: PassthroughSubject<AuthenticationStartScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(authenticationService: AuthenticationServiceProtocol,
|
||||
provisioningParameters: AccountProvisioningParameters?,
|
||||
isBugReportServiceEnabled: Bool,
|
||||
appSettings: AppSettings,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.authenticationService = authenticationService
|
||||
self.provisioningParameters = provisioningParameters
|
||||
self.appSettings = appSettings
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let isQRCodeScanningSupported = !ProcessInfo.processInfo.isiOSAppOnMac
|
||||
|
||||
let initialViewState = if !appSettings.allowOtherAccountProviders {
|
||||
// We don't show the create account button when custom providers are disallowed.
|
||||
// The assumption here being that if you're running a custom app, your users will already be created.
|
||||
AuthenticationStartScreenViewState(serverName: appSettings.accountProviders.count == 1 ? appSettings.accountProviders[0] : nil,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
} else if let provisioningParameters {
|
||||
// We only show the "Sign in to …" button when using a provisioning link.
|
||||
AuthenticationStartScreenViewState(serverName: provisioningParameters.accountProvider,
|
||||
showCreateAccountButton: false,
|
||||
showQRCodeLoginButton: false,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
} else {
|
||||
// The default configuration.
|
||||
AuthenticationStartScreenViewState(serverName: nil,
|
||||
showCreateAccountButton: appSettings.showCreateAccountButton,
|
||||
showQRCodeLoginButton: isQRCodeScanningSupported,
|
||||
showReportProblemButton: isBugReportServiceEnabled)
|
||||
}
|
||||
|
||||
super.init(initialViewState: initialViewState)
|
||||
}
|
||||
|
||||
override func process(viewAction: AuthenticationStartScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .updateWindow(let window):
|
||||
guard state.window != window else { return }
|
||||
state.window = window
|
||||
case .loginWithQR:
|
||||
actionsSubject.send(.loginWithQR)
|
||||
case .login:
|
||||
Task { await login() }
|
||||
case .register:
|
||||
actionsSubject.send(.register)
|
||||
case .reportProblem:
|
||||
actionsSubject.send(.reportProblem)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func login() async {
|
||||
if let serverName = state.serverName {
|
||||
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
|
||||
} else {
|
||||
actionsSubject.send(.login) // No need to configure anything here, continue the flow.
|
||||
}
|
||||
}
|
||||
|
||||
private func configureAccountProvider(_ accountProvider: String, loginHint: String? = nil) async {
|
||||
startLoading()
|
||||
defer { stopLoading() }
|
||||
|
||||
guard case .success = await authenticationService.configure(for: accountProvider, flow: .login) else {
|
||||
// As the server was provisioned, we don't worry about the specifics and show a generic error to the user.
|
||||
displayError()
|
||||
return
|
||||
}
|
||||
|
||||
guard authenticationService.homeserver.value.loginMode.supportsOIDCFlow else {
|
||||
actionsSubject.send(.loginDirectlyWithPassword(loginHint: loginHint))
|
||||
return
|
||||
}
|
||||
|
||||
guard let window = state.window else {
|
||||
displayError()
|
||||
return
|
||||
}
|
||||
|
||||
switch await authenticationService.urlForOIDCLogin(loginHint: loginHint) {
|
||||
case .success(let oidcData):
|
||||
actionsSubject.send(.loginDirectlyWithOIDC(data: oidcData, window: window))
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
}
|
||||
|
||||
private let loadingIndicatorID = "\(AuthenticationStartScreenViewModel.self)-Loading"
|
||||
|
||||
private func startLoading() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorID,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func stopLoading() {
|
||||
userIndicatorController.retractIndicatorWithId(loadingIndicatorID)
|
||||
}
|
||||
|
||||
private func displayError() {
|
||||
state.bindings.alertInfo = AlertInfo(id: .genericError)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationStartScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> { get }
|
||||
var context: AuthenticationStartScreenViewModelType.Context { get }
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Copyright 2023, 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The app's logo styled to fit on various launch pages.
|
||||
struct AuthenticationStartLogo: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
/// Set to `true` when using on top of `Asset.Images.launchBackground`
|
||||
let isOnGradient: Bool
|
||||
|
||||
/// Extra padding needed to avoid cropping the shadows.
|
||||
private let extra: CGFloat = 64
|
||||
/// The shape that the logo is composed on top of.
|
||||
private let outerShape = RoundedRectangle(cornerRadius: 44)
|
||||
private let outerShapeShadowColor = Color(red: 0.11, green: 0.11, blue: 0.13)
|
||||
private var isLight: Bool { colorScheme == .light }
|
||||
|
||||
var body: some View {
|
||||
Image(asset: Asset.Images.appLogo)
|
||||
.background {
|
||||
Circle()
|
||||
.inset(by: 1)
|
||||
.shadow(color: .black.opacity(!isLight && isOnGradient ? 0.3 : 0.4),
|
||||
radius: 12.57143,
|
||||
y: 6.28571)
|
||||
|
||||
Circle()
|
||||
.inset(by: 1)
|
||||
.shadow(color: .black.opacity(0.5),
|
||||
radius: 12.57143,
|
||||
y: 6.28571)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
.padding(24)
|
||||
.background {
|
||||
Color.white
|
||||
.opacity(isLight ? 0.23 : isOnGradient ? 0.05 : 0.13)
|
||||
}
|
||||
.clipShape(outerShape)
|
||||
.overlay {
|
||||
outerShape
|
||||
.inset(by: 0.25)
|
||||
.stroke(.white.opacity(isLight ? 1 : isOnGradient ? 0.9 : 0.25), lineWidth: 0.5)
|
||||
.blendMode(isLight ? .normal : .overlay)
|
||||
}
|
||||
.padding(extra)
|
||||
.background {
|
||||
ZStack {
|
||||
if !isLight, isOnGradient {
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.padding(extra)
|
||||
.shadow(color: .black.opacity(0.5),
|
||||
radius: 32.91666,
|
||||
y: 1.05333)
|
||||
} else {
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.padding(extra)
|
||||
.shadow(color: outerShapeShadowColor.opacity(isLight ? 0.23 : 0.08),
|
||||
radius: 16,
|
||||
y: 8)
|
||||
|
||||
outerShape
|
||||
.inset(by: 1)
|
||||
.padding(extra)
|
||||
.shadow(color: outerShapeShadowColor.opacity(0.5),
|
||||
radius: 16,
|
||||
y: 8)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
}
|
||||
.mask {
|
||||
outerShape
|
||||
.inset(by: -extra / 2)
|
||||
.stroke(lineWidth: extra)
|
||||
.padding(extra)
|
||||
}
|
||||
}
|
||||
.padding(-extra)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
/// The screen shown at the beginning of the onboarding flow.
|
||||
struct AuthenticationStartScreen: View {
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
let context: AuthenticationStartScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.hidden)
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if context.viewState.showReportProblemButton {
|
||||
Button {
|
||||
context.send(viewAction: .reportProblem)
|
||||
} label: {
|
||||
Text(L10n.commonReportAProblem)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.reportAProblem)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.background {
|
||||
AuthenticationStartScreenBackgroundImage()
|
||||
}
|
||||
.introspect(.window, on: .supportedVersions) { window in
|
||||
context.send(viewAction: .updateWindow(window))
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
if verticalSizeClass == .regular {
|
||||
Spacer()
|
||||
|
||||
AuthenticationStartLogo(isOnGradient: true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenOnboardingWelcomeTitle)
|
||||
.font(.compound.headingLGBold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(L10n.screenOnboardingWelcomeMessage(InfoPlistReader.main.productionAppName))
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal, 16)
|
||||
.readableFrame()
|
||||
}
|
||||
|
||||
/// The main action buttons.
|
||||
var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
if context.viewState.showQRCodeLoginButton {
|
||||
Button { context.send(viewAction: .loginWithQR) } label: {
|
||||
Label(L10n.screenOnboardingSignInWithQrCode, icon: \.qrCode)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signInWithQr)
|
||||
}
|
||||
|
||||
Button { context.send(viewAction: .login) } label: {
|
||||
Text(context.viewState.loginButtonTitle)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
.accessibilityIdentifier(A11yIdentifiers.authenticationStartScreen.signIn)
|
||||
|
||||
if context.viewState.showCreateAccountButton {
|
||||
Button { context.send(viewAction: .register) } label: {
|
||||
Text(L10n.screenCreateAccountTitle)
|
||||
}
|
||||
.buttonStyle(.compound(.tertiary))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
|
||||
.readableFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationStartScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel()
|
||||
static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false)
|
||||
static let provisionedViewModel = makeViewModel(provisionedServerName: "example.com")
|
||||
|
||||
static var previews: some View {
|
||||
AuthenticationStartScreen(context: viewModel.context)
|
||||
.previewDisplayName("Default")
|
||||
AuthenticationStartScreen(context: bugReportDisabledViewModel.context)
|
||||
.previewDisplayName("Bug report disabled")
|
||||
AuthenticationStartScreen(context: provisionedViewModel.context)
|
||||
.previewDisplayName("Provisioned")
|
||||
}
|
||||
|
||||
static func makeViewModel(isBugReportServiceEnabled: Bool = true, provisionedServerName: String? = nil) -> AuthenticationStartScreenViewModel {
|
||||
AuthenticationStartScreenViewModel(authenticationService: AuthenticationService.mock,
|
||||
provisioningParameters: provisionedServerName.map { .init(accountProvider: $0, loginHint: nil) },
|
||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright 2023, 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The background gradient shown on the launch, splash and onboarding screens.
|
||||
struct AuthenticationStartScreenBackgroundImage: View {
|
||||
var body: some View {
|
||||
Image(asset: Asset.Images.launchBackground)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.ignoresSafeArea()
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user