Renaming screens returns (#829)

* Rename: StartChat -> StartChatScreen

* Rename: Login -> LoginScreen

* Rename: ServerSelection -> ServerSelectionScreen

* Rename: SoftLogout -> SoftLogoutScreen
This commit is contained in:
Stefan Ceriu
2023-04-25 12:13:03 +03:00
committed by GitHub
parent 24572c6582
commit b726cd149a
31 changed files with 286 additions and 286 deletions
@@ -0,0 +1,68 @@
//
// 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
import SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
enum MockSoftLogoutScreenState: String, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case emptyPassword
case enteredPassword
case oidc
case unsupported
case keyBackupNeeded
/// Generate the view struct for the screen state.
@MainActor var viewModel: SoftLogoutScreenViewModel {
let credentials = SoftLogoutScreenCredentials(userId: "@mock:matrix.org",
homeserverName: "matrix.org",
userDisplayName: "mock",
deviceId: nil)
switch self {
case .emptyPassword:
return SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: false)
case .enteredPassword:
return SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: false,
password: "12345678")
case .oidc:
return SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockOIDC,
keyBackupNeeded: false)
case .unsupported:
return SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockUnsupported,
keyBackupNeeded: false)
case .keyBackupNeeded:
return SoftLogoutScreenViewModel(credentials: credentials,
homeserver: .mockMatrixDotOrg,
keyBackupNeeded: true)
}
}
}
extension MockSoftLogoutScreenState: Identifiable {
var id: String {
rawValue
}
}
@@ -0,0 +1,170 @@
//
// 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 AppAuth
import SwiftUI
struct SoftLogoutScreenCoordinatorParameters {
let authenticationService: AuthenticationServiceProxyProtocol
let credentials: SoftLogoutScreenCredentials
let keyBackupNeeded: Bool
}
enum SoftLogoutScreenCoordinatorResult: CustomStringConvertible {
/// Login was successful.
case signedIn(UserSessionProtocol)
/// Clear all user data
case clearAllData
/// A string representation of the result, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .signedIn:
return "signedIn"
case .clearAllData:
return "clearAllData"
}
}
}
final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
private let parameters: SoftLogoutScreenCoordinatorParameters
private var viewModel: SoftLogoutScreenViewModelProtocol
private let hostingController: UIViewController
/// Passed to the OIDC service to provide a view controller from which to present the authentication session.
private let oidcUserAgent: OIDExternalUserAgentIOS?
/// The wizard used to handle the registration flow.
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
var callback: (@MainActor (SoftLogoutScreenCoordinatorResult) -> Void)?
@MainActor init(parameters: SoftLogoutScreenCoordinatorParameters) {
self.parameters = parameters
let homeserver = parameters.authenticationService.homeserver
viewModel = SoftLogoutScreenViewModel(credentials: parameters.credentials,
homeserver: homeserver,
keyBackupNeeded: parameters.keyBackupNeeded)
hostingController = UIHostingController(rootView: SoftLogoutScreen(context: viewModel.context))
oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController)
}
// MARK: - Public
func start() {
viewModel.callback = { [weak self] result in
guard let self else { return }
MXLog.info("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).")
switch result {
case .login(let password):
self.login(withPassword: password)
case .forgotPassword:
self.showForgotPasswordScreen()
case .clearAllData:
self.callback?(.clearAllData)
case .continueWithOIDC:
self.loginWithOIDC()
}
}
}
func stop() {
stopLoading()
}
func toPresentable() -> AnyView {
AnyView(SoftLogoutScreen(context: viewModel.context))
}
// MARK: - Private
static let loadingIndicatorIdentifier = "SoftLogoutLoading"
/// Show an activity indicator whilst loading.
@MainActor private func startLoading() {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
}
/// Hide the currently displayed activity indicator.
@MainActor private func stopLoading() {
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
/// Shows the forgot password screen.
@MainActor private func showForgotPasswordScreen() {
viewModel.displayError(.alert("Not implemented."))
}
/// Login with the supplied username and password.
@MainActor private func login(withPassword password: String) {
let username = parameters.credentials.userId
startLoading()
Task {
switch await authenticationService.login(username: username,
password: password,
initialDeviceName: UIDevice.current.initialDeviceName,
deviceId: parameters.credentials.deviceId) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
private func loginWithOIDC() {
guard let oidcUserAgent else {
handleError(AuthenticationServiceError.oidcError(.notSupported))
return
}
startLoading()
Task {
switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) {
case .success(let userSession):
callback?(.signedIn(userSession))
stopLoading()
case .failure(let error):
stopLoading()
handleError(error)
}
}
}
/// Processes an error to either update the flow or display it to the user.
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidCredentials:
viewModel.displayError(.alert(L10n.screenLoginErrorInvalidCredentials))
case .accountDeactivated:
viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount))
default:
viewModel.displayError(.alert(L10n.errorUnknown))
}
}
}
@@ -0,0 +1,101 @@
//
// 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 SoftLogoutScreenCredentials {
let userId: String
let homeserverName: String
let userDisplayName: String
let deviceId: String?
}
enum SoftLogoutScreenViewModelAction: CustomStringConvertible {
/// Login with password
case login(String)
/// Forgot password
case forgotPassword
/// Clear all user data
case clearAllData
/// Continue using OIDC.
case continueWithOIDC
/// A string representation of the result, ignoring any associated values that could leak PII.
var description: String {
switch self {
case .login:
return "login"
case .forgotPassword:
return "forgotPassword"
case .clearAllData:
return "clearAllData"
case .continueWithOIDC:
return "continueWithOIDC"
}
}
}
struct SoftLogoutScreenViewState: BindableState {
/// Soft logout credentials
var credentials: SoftLogoutScreenCredentials
/// Data about the selected homeserver.
var homeserver: LoginHomeserver
/// Flag indicating soft logged out user needs backup for some keys
var keyBackupNeeded: Bool
/// View state that can be bound to from SwiftUI.
var bindings: SoftLogoutScreenBindings
/// The types of login supported by the homeserver.
var loginMode: LoginMode { homeserver.loginMode }
/// Whether to show recover encryption keys message
var showRecoverEncryptionKeysMessage: Bool {
keyBackupNeeded
}
/// `true` when valid credentials have been entered and a homeserver has been loaded.
var canSubmit: Bool {
!bindings.password.isEmpty
}
}
struct SoftLogoutScreenBindings {
/// The password input by the user.
var password: String
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<SoftLogoutScreenErrorType>?
}
enum SoftLogoutScreenViewAction {
/// Login.
case login
/// Forgot password
case forgotPassword
/// Clear all user data.
case clearAllData
/// Continue using OIDC.
case continueWithOIDC
}
enum SoftLogoutScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
/// An unknown error occurred.
case unknown
}
@@ -0,0 +1,59 @@
//
// 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
typealias SoftLogoutScreenViewModelType = StateStoreViewModel<SoftLogoutScreenViewState, SoftLogoutScreenViewAction>
class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreenViewModelProtocol {
var callback: (@MainActor (SoftLogoutScreenViewModelAction) -> Void)?
init(credentials: SoftLogoutScreenCredentials,
homeserver: LoginHomeserver,
keyBackupNeeded: Bool,
password: String = "") {
let bindings = SoftLogoutScreenBindings(password: password)
let viewState = SoftLogoutScreenViewState(credentials: credentials,
homeserver: homeserver,
keyBackupNeeded: keyBackupNeeded,
bindings: bindings)
super.init(initialViewState: viewState)
}
override func process(viewAction: SoftLogoutScreenViewAction) {
switch viewAction {
case .login:
callback?(.login(state.bindings.password))
case .forgotPassword:
callback?(.forgotPassword)
case .clearAllData:
callback?(.clearAllData)
case .continueWithOIDC:
callback?(.continueWithOIDC)
}
}
@MainActor func displayError(_ type: SoftLogoutScreenErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: L10n.commonError,
message: message)
case .unknown:
state.bindings.alertInfo = AlertInfo(id: type)
}
}
}
@@ -0,0 +1,25 @@
//
// 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
protocol SoftLogoutScreenViewModelProtocol {
var callback: (@MainActor (SoftLogoutScreenViewModelAction) -> Void)? { get set }
var context: SoftLogoutScreenViewModelType.Context { get }
/// Display an error to the user.
@MainActor func displayError(_ type: SoftLogoutScreenErrorType)
}
@@ -0,0 +1,194 @@
//
// 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 SoftLogoutScreen: View {
@State private var showingClearDataConfirmation = false
/// The focus state of the password text field.
@FocusState private var isPasswordFocused: Bool
@ObservedObject var context: SoftLogoutScreenViewModel.Context
var body: some View {
ScrollView {
VStack(spacing: 0) {
header
.padding(.top, UIConstants.titleTopPaddingToNavigationBar)
.padding(.bottom, 36)
switch context.viewState.loginMode {
case .password:
loginForm
case .oidc:
oidcButton
default:
loginUnavailableText
}
clearDataForm
.padding(.top, 16)
}
.readableFrame()
.padding(.horizontal, 16)
.padding(.bottom, 16)
}
.background(Color.element.background.ignoresSafeArea())
.alert(item: $context.alertInfo) { $0.alert }
}
/// The title, message and icon at the top of the screen.
var header: some View {
VStack(alignment: .leading, spacing: 16) {
Text(UntranslatedL10n.softLogoutSigninTitle)
.font(.element.title2Bold)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.title)
Text(UntranslatedL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userId))
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.message)
if context.viewState.showRecoverEncryptionKeysMessage {
Text(UntranslatedL10n.softLogoutSigninE2eWarningNotice)
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
}
}
}
/// The form with text fields for username and password, along with a submit button.
var loginForm: some View {
VStack(spacing: 14) {
SecureField(L10n.commonPassword, text: $context.password)
.focused($isPasswordFocused)
.textFieldStyle(.elementInput())
.textContentType(.password)
.submitLabel(.done)
.onSubmit(submit)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.password)
Button { context.send(viewAction: .forgotPassword) } label: {
Text(UntranslatedL10n.softLogoutForgotPassword)
.font(.element.body)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.bottom, 8)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.forgotPassword)
Button(action: submit) {
Text(L10n.actionNext)
}
.buttonStyle(.elementAction(.xLarge))
.disabled(!context.viewState.canSubmit)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.next)
}
}
/// The OIDC button that can be used for login.
var oidcButton: some View {
Button { context.send(viewAction: .continueWithOIDC) } label: {
Text(L10n.actionContinue)
}
.buttonStyle(.elementAction(.xLarge))
}
/// Text shown if neither password or OIDC login is supported.
var loginUnavailableText: some View {
Text(L10n.screenLoginErrorUnsupportedAuthentication)
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.element.primaryContent)
.frame(maxWidth: .infinity)
}
/// The text field and submit button where the user enters an email address.
var clearDataForm: some View {
VStack(alignment: .leading, spacing: 12) {
Text(UntranslatedL10n.softLogoutClearDataTitle)
.font(.element.title2Bold)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.clearDataTitle)
Text(UntranslatedL10n.softLogoutClearDataNotice)
.font(.element.body)
.multilineTextAlignment(.leading)
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.clearDataMessage)
.padding(.bottom, 12)
Button(action: clearData) {
Text(UntranslatedL10n.softLogoutClearDataSubmit)
}
.buttonStyle(.elementAction(.xLarge, color: .element.alert))
.accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.clearData)
.alert(UntranslatedL10n.softLogoutClearDataDialogTitle,
isPresented: $showingClearDataConfirmation) {
Button(L10n.screenSignoutConfirmationDialogSubmit,
role: .destructive,
action: clearData)
} message: {
Text(UntranslatedL10n.softLogoutClearDataDialogContent)
}
}
}
/// Sends the `login` view action so long as a valid email address has been input.
func submit() {
guard context.viewState.canSubmit else { return }
context.send(viewAction: .login)
}
/// Sends the `forgotPassword` view action.
func forgotPassword() {
context.send(viewAction: .forgotPassword)
}
/// Sends the `clearAllData` view action.
func clearData() {
context.send(viewAction: .clearAllData)
}
}
// MARK: - Previews
struct SoftLogoutScreen_Previews: PreviewProvider {
static var previews: some View {
ForEach(MockSoftLogoutScreenState.allCases) { state in
screen(for: state.viewModel)
}
}
static func screen(for viewModel: SoftLogoutScreenViewModel) -> some View {
NavigationStack {
SoftLogoutScreen(context: viewModel.context)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button { } label: {
Text("\(Image(systemName: "chevron.backward")) Back")
}
}
}
}
}
}