mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
@@ -23,8 +23,6 @@ public enum UntranslatedL10n {
|
||||
public static var softLogoutClearDataSubmit: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_submit") }
|
||||
/// Clear personal data
|
||||
public static var softLogoutClearDataTitle: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_title") }
|
||||
/// Forgot password
|
||||
public static var softLogoutForgotPassword: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_forgot_password") }
|
||||
/// Sign in to recover encryption keys stored exclusively on this device. You need them to read all of your secure messages on any device.
|
||||
public static var softLogoutSigninE2eWarningNotice: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_signin_e2e_warning_notice") }
|
||||
/// Your homeserver (%1$s) admin has signed you out of your account %2$s (%3$s).
|
||||
|
||||
@@ -26,6 +26,7 @@ struct A11yIdentifiers {
|
||||
static let reportContent = ReportContent()
|
||||
static let roomScreen = RoomScreen()
|
||||
static let roomDetailsScreen = RoomDetailsScreen()
|
||||
static let serverConfirmationScreen = ServerConfirmationScreen()
|
||||
static let sessionVerificationScreen = SessionVerificationScreen()
|
||||
static let softLogoutScreen = SoftLogoutScreen()
|
||||
static let startChatScreen = StartChatScreen()
|
||||
@@ -73,7 +74,6 @@ struct A11yIdentifiers {
|
||||
let emailUsername = "login-email_username"
|
||||
let password = "login-password"
|
||||
let `continue` = "login-continue"
|
||||
let changeServer = "login-change_server"
|
||||
let oidc = "login-oidc"
|
||||
let unsupportedServer = "login-unsupported_server"
|
||||
}
|
||||
@@ -106,6 +106,11 @@ struct A11yIdentifiers {
|
||||
let unignore = "room_member_details-unignore"
|
||||
}
|
||||
|
||||
struct ServerConfirmationScreen {
|
||||
let `continue` = "server_confirmation-continue"
|
||||
let changeServer = "server_confirmation-change_server"
|
||||
}
|
||||
|
||||
struct SessionVerificationScreen {
|
||||
let requestVerification = "session_verification-request_verification"
|
||||
let startSasVerification = "session_verification-start_sas_verification"
|
||||
@@ -115,7 +120,7 @@ struct A11yIdentifiers {
|
||||
let verificationComplete = "session_verification-verification_complete"
|
||||
let close = "session_verification-close"
|
||||
}
|
||||
|
||||
|
||||
struct SoftLogoutScreen {
|
||||
let title = "soft_logout-title"
|
||||
let message = "soft_logout-message"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@@ -26,6 +27,8 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
@@ -64,17 +67,20 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
switch await authenticationService.configure(for: ServiceLocator.shared.settings.defaultHomeserverAddress) {
|
||||
case .success:
|
||||
stopLoading()
|
||||
showLoginScreen()
|
||||
showServerConfirmationScreen()
|
||||
case .failure:
|
||||
stopLoading()
|
||||
showServerSelectionScreen()
|
||||
showServerSelectionScreen(isModallyPresented: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func showServerSelectionScreen() {
|
||||
private func showServerSelectionScreen(isModallyPresented: Bool) {
|
||||
let navigationCoordinator = NavigationStackCoordinator()
|
||||
let userIndicatorController: UserIndicatorControllerProtocol! = isModallyPresented ? UserIndicatorController(rootCoordinator: navigationCoordinator) : ServiceLocator.shared.userIndicatorController
|
||||
|
||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
isModallyPresented: false)
|
||||
userIndicatorController: userIndicatorController,
|
||||
isModallyPresented: isModallyPresented)
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@@ -82,18 +88,46 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .updated:
|
||||
self.showLoginScreen()
|
||||
if isModallyPresented {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
} else {
|
||||
showLoginScreen()
|
||||
}
|
||||
case .dismiss:
|
||||
MXLog.failure("ServerSelectionScreen is requesting dismiss when part of a stack.")
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if isModallyPresented {
|
||||
navigationCoordinator.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||
} 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 .confirm:
|
||||
self.showLoginScreen()
|
||||
case .changeServer:
|
||||
self.showServerSelectionScreen(isModallyPresented: true)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
navigationStackCoordinator: navigationStackCoordinator)
|
||||
let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService)
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
|
||||
@@ -20,8 +20,6 @@ import SwiftUI
|
||||
struct LoginScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
/// The navigation router used to present the server selection screen.
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
}
|
||||
|
||||
enum LoginScreenCoordinatorAction {
|
||||
@@ -37,7 +35,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
private let oidcAuthenticationPresenter: OIDCAuthenticationPresenter
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
||||
|
||||
var callback: (@MainActor (LoginScreenCoordinatorAction) -> Void)?
|
||||
|
||||
@@ -46,7 +43,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: LoginScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver)
|
||||
viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value)
|
||||
|
||||
oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService)
|
||||
}
|
||||
@@ -58,8 +55,6 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .selectServer:
|
||||
self.presentServerSelectionScreen()
|
||||
case .parseUsername(let username):
|
||||
self.parseUsername(username)
|
||||
case .forgotPassword:
|
||||
@@ -193,35 +188,10 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
/// Updates the view model with a different homeserver.
|
||||
private func updateViewModel() {
|
||||
viewModel.update(homeserver: authenticationService.homeserver)
|
||||
viewModel.update(homeserver: authenticationService.homeserver.value)
|
||||
indicateSuccess()
|
||||
}
|
||||
|
||||
/// Presents the server selection screen as a modal.
|
||||
private func presentServerSelectionScreen() {
|
||||
let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
isModallyPresented: false)
|
||||
|
||||
let coordinator = ServerSelectionScreenCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] action in
|
||||
guard let self, let coordinator else { return }
|
||||
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
|
||||
}
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
/// Handles the result from the server selection modal, dismissing it after updating the view.
|
||||
private func serverSelectionCoordinator(_ coordinator: ServerSelectionScreenCoordinator,
|
||||
didCompleteWith action: ServerSelectionScreenCoordinatorAction) {
|
||||
if action == .updated {
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
private func showForgotPasswordScreen() {
|
||||
viewModel.displayError(.alert("Not implemented."))
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import Foundation
|
||||
|
||||
enum LoginScreenViewModelAction: CustomStringConvertible {
|
||||
/// The user would like to select another server.
|
||||
case selectServer
|
||||
/// Parse the username and update the homeserver if included.
|
||||
case parseUsername(String)
|
||||
/// The user would like to reset their password.
|
||||
@@ -31,8 +29,6 @@ enum LoginScreenViewModelAction: CustomStringConvertible {
|
||||
/// A string representation of the action, ignoring any associated values that could leak PII.
|
||||
var description: String {
|
||||
switch self {
|
||||
case .selectServer:
|
||||
return "selectServer"
|
||||
case .parseUsername:
|
||||
return "parseUsername"
|
||||
case .forgotPassword:
|
||||
@@ -77,8 +73,6 @@ struct LoginScreenBindings {
|
||||
}
|
||||
|
||||
enum LoginScreenViewAction {
|
||||
/// The user would like to select another server.
|
||||
case selectServer
|
||||
/// Parse the username to detect if a homeserver is included.
|
||||
case parseUsername
|
||||
/// The user would like to reset their password.
|
||||
|
||||
@@ -30,8 +30,6 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
|
||||
|
||||
override func process(viewAction: LoginScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .selectServer:
|
||||
callback?(.selectServer)
|
||||
case .parseUsername:
|
||||
callback?(.parseUsername(state.bindings.username))
|
||||
case .forgotPassword:
|
||||
|
||||
@@ -31,9 +31,6 @@ struct LoginScreen: View {
|
||||
.padding(.top, UIConstants.titleTopPaddingToNavigationBar)
|
||||
.padding(.bottom, 32)
|
||||
|
||||
serverInfo
|
||||
.padding(.bottom, 32)
|
||||
|
||||
switch context.viewState.loginMode {
|
||||
case .password:
|
||||
loginForm
|
||||
@@ -51,19 +48,18 @@ struct LoginScreen: View {
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
/// The header containing a Welcome Back title.
|
||||
/// The header containing the title and icon.
|
||||
var header: some View {
|
||||
Text(L10n.screenLoginTitle)
|
||||
.font(.compound.headingLGBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
|
||||
/// The sever information section that includes a button to select a different server.
|
||||
var serverInfo: some View {
|
||||
LoginServerInfoSection(address: context.viewState.homeserver.address) {
|
||||
context.send(viewAction: .selectServer)
|
||||
VStack(spacing: 8) {
|
||||
AuthenticationIconImage(image: Image(systemName: "lock.fill"))
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(L10n.screenLoginTitleWithHomeserver(context.viewState.homeserver.address))
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
/// The form with text fields for username and password, along with a submit button.
|
||||
|
||||
@@ -1,65 +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
|
||||
|
||||
/// A view that shows information about the chosen homeserver,
|
||||
/// along with an edit button to pick a different one.
|
||||
struct LoginServerInfoSection: View {
|
||||
// MARK: - Public
|
||||
|
||||
/// The address shown for the server.
|
||||
let address: String
|
||||
/// The action performed when tapping the edit button.
|
||||
let editAction: () -> Void
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.screenLoginServerHeader)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
Button(action: editAction) {
|
||||
HStack {
|
||||
Text(address)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.compound.bodyMD.bold())
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 14).fill(Color.element.system))
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.loginScreen.changeServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginServerInfoSection_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoginServerInfoSection(address: "matrix.org", editAction: { })
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct ServerConfirmationScreenCoordinatorParameters {
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
}
|
||||
|
||||
enum ServerConfirmationScreenCoordinatorAction {
|
||||
case confirm
|
||||
case changeServer
|
||||
}
|
||||
|
||||
final class ServerConfirmationScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ServerConfirmationScreenCoordinatorParameters
|
||||
private var viewModel: ServerConfirmationScreenViewModelProtocol
|
||||
private let actionsSubject: PassthroughSubject<ServerConfirmationScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
|
||||
var actions: AnyPublisher<ServerConfirmationScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: ServerConfirmationScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService,
|
||||
authenticationFlow: parameters.authenticationFlow)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .confirm:
|
||||
self.actionsSubject.send(.confirm)
|
||||
case .changeServer:
|
||||
self.actionsSubject.send(.changeServer)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ServerConfirmationScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// 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 ServerConfirmationScreenViewModelAction {
|
||||
/// The user would like to continue with the current homeserver.
|
||||
case confirm
|
||||
/// The user would like to change to a different homeserver.
|
||||
case changeServer
|
||||
}
|
||||
|
||||
struct ServerConfirmationScreenViewState: BindableState {
|
||||
/// The homeserver address input by the user.
|
||||
var homeserverAddress: String
|
||||
/// The flow being attempted on the selected homeserver.
|
||||
let authenticationFlow: AuthenticationFlow
|
||||
|
||||
/// The screen's title.
|
||||
var title: String {
|
||||
switch authenticationFlow {
|
||||
case .login:
|
||||
return L10n.screenServerConfirmationTitleLogin(homeserverAddress)
|
||||
case .register:
|
||||
return L10n.screenServerConfirmationTitleRegister(homeserverAddress)
|
||||
}
|
||||
}
|
||||
|
||||
/// The message shown beneath the title.
|
||||
var message: String {
|
||||
switch authenticationFlow {
|
||||
case .login:
|
||||
if homeserverAddress == "matrix.org" {
|
||||
return L10n.screenServerConfirmationMessageLoginMatrixDotOrg
|
||||
} else if homeserverAddress == "element.io" {
|
||||
return L10n.screenServerConfirmationMessageLoginElementDotIo
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case .register:
|
||||
return L10n.screenServerConfirmationMessageRegister
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerConfirmationScreenViewAction {
|
||||
/// The user would like to continue with the current homeserver.
|
||||
case confirm
|
||||
/// The user would like to change to a different homeserver.
|
||||
case changeServer
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// 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 ServerConfirmationScreenViewModelType = StateStoreViewModel<ServerConfirmationScreenViewState, ServerConfirmationScreenViewAction>
|
||||
|
||||
class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<ServerConfirmationScreenViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol, authenticationFlow: AuthenticationFlow) {
|
||||
super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: authenticationService.homeserver.value.address,
|
||||
authenticationFlow: authenticationFlow))
|
||||
|
||||
authenticationService.homeserver
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] homeserver in
|
||||
guard let self else { return }
|
||||
state.homeserverAddress = homeserver.address
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ServerConfirmationScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .confirm:
|
||||
actionsSubject.send(.confirm)
|
||||
case .changeServer:
|
||||
actionsSubject.send(.changeServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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 ServerConfirmationScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> { get }
|
||||
var context: ServerConfirmationScreenViewModelType.Context { get }
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// 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 ServerConfirmationScreen: View {
|
||||
@ObservedObject var context: ServerConfirmationScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
header
|
||||
.padding(.top, UIConstants.iconTopPaddingToNavigationBar)
|
||||
.padding(.horizontal, 16)
|
||||
.readableFrame()
|
||||
}
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
buttons
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 16)
|
||||
.readableFrame()
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
/// The main content of the view to be shown in a scroll view.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
AuthenticationIconImage(image: Image(systemName: "person.crop.circle.fill"))
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(context.viewState.title)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(context.viewState.message)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
/// The action buttons shown at the bottom of the view.
|
||||
var buttons: some View {
|
||||
VStack(spacing: 24) {
|
||||
Button { context.send(viewAction: .confirm) } label: {
|
||||
Text(L10n.actionContinue)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue)
|
||||
|
||||
Button { context.send(viewAction: .changeServer) } label: {
|
||||
Text(L10n.screenServerConfirmationChangeServer)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.changeServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ServerConfirmationScreen_Previews: PreviewProvider {
|
||||
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
|
||||
authenticationFlow: .login)
|
||||
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
|
||||
authenticationFlow: .register)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
ServerConfirmationScreen(context: loginViewModel.context)
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
}
|
||||
.previewDisplayName("Login")
|
||||
|
||||
NavigationStack {
|
||||
ServerConfirmationScreen(context: registerViewModel.context)
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
}
|
||||
.previewDisplayName("Register")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -39,7 +39,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: ServerSelectionScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
|
||||
viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address,
|
||||
isModallyPresented: parameters.isModallyPresented)
|
||||
userIndicatorController = parameters.userIndicatorController
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
let homeserver = parameters.authenticationService.homeserver
|
||||
viewModel = SoftLogoutScreenViewModel(credentials: parameters.credentials,
|
||||
homeserver: homeserver,
|
||||
homeserver: homeserver.value,
|
||||
keyBackupNeeded: parameters.keyBackupNeeded)
|
||||
|
||||
oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService)
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ struct SoftLogoutScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.password)
|
||||
|
||||
Button { context.send(viewAction: .forgotPassword) } label: {
|
||||
Text(UntranslatedL10n.softLogoutForgotPassword)
|
||||
Text(L10n.actionForgotPassword)
|
||||
.font(.compound.bodyLG)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
@@ -21,7 +22,9 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
private let authenticationService: AuthenticationService
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
|
||||
private(set) var homeserver = LoginHomeserver(address: ServiceLocator.shared.settings.defaultHomeserverAddress, loginMode: .unknown)
|
||||
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never> = .init(LoginHomeserver(address: ServiceLocator.shared.settings.defaultHomeserverAddress,
|
||||
loginMode: .unknown))
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol) {
|
||||
self.userSessionStore = userSessionStore
|
||||
@@ -60,7 +63,7 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
self.homeserver = homeserver
|
||||
homeserverSubject.send(homeserver)
|
||||
return .success(())
|
||||
} catch AuthenticationError.SlidingSyncNotAvailable {
|
||||
MXLog.info("User entered a homeserver that isn't configured for sliding sync.")
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
/// Represents a particular authentication flow.
|
||||
enum AuthenticationFlow {
|
||||
/// The flow for signing in to an existing account.
|
||||
case login
|
||||
/// The flow for creating a new account.
|
||||
case register
|
||||
}
|
||||
|
||||
enum AuthenticationServiceError: Error {
|
||||
/// An error occurred during OIDC authentication.
|
||||
case oidcError(OIDCError)
|
||||
@@ -29,7 +37,8 @@ enum AuthenticationServiceError: Error {
|
||||
}
|
||||
|
||||
protocol AuthenticationServiceProxyProtocol {
|
||||
var homeserver: LoginHomeserver { get }
|
||||
/// The currently configured homeserver.
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
|
||||
|
||||
@@ -14,27 +14,33 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
let validCredentials = (username: "alice", password: "12345678")
|
||||
|
||||
private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg
|
||||
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
|
||||
init(homeserver: LoginHomeserver = .mockMatrixDotOrg) {
|
||||
homeserverSubject = .init(homeserver)
|
||||
}
|
||||
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
// Map the address to the mock homeservers
|
||||
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
|
||||
homeserver = .mockMatrixDotOrg
|
||||
homeserverSubject.send(.mockMatrixDotOrg)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) {
|
||||
homeserver = .mockOIDC
|
||||
homeserverSubject.send(.mockOIDC)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) {
|
||||
homeserver = .mockBasicServer
|
||||
homeserverSubject.send(.mockBasicServer)
|
||||
return .success(())
|
||||
} else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) {
|
||||
homeserver = .mockUnsupported
|
||||
homeserverSubject.send(.mockUnsupported)
|
||||
return .success(())
|
||||
} else {
|
||||
// Otherwise fail with an invalid server.
|
||||
|
||||
@@ -75,8 +75,19 @@ class MockScreen: Identifiable {
|
||||
switch id {
|
||||
case .login:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .serverConfirmationLogin:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = ServerConfirmationScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(homeserver: .mockMatrixDotOrg),
|
||||
authenticationFlow: .login))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .serverConfirmationRegister:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = ServerConfirmationScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(homeserver: .mockOIDC),
|
||||
authenticationFlow: .register))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .serverSelection:
|
||||
|
||||
@@ -18,6 +18,8 @@ import Foundation
|
||||
|
||||
enum UITestsScreenIdentifier: String {
|
||||
case login
|
||||
case serverConfirmationLogin
|
||||
case serverConfirmationRegister
|
||||
case serverSelection
|
||||
case serverSelectionNonModal
|
||||
case authenticationFlow
|
||||
|
||||
Reference in New Issue
Block a user