mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
Add choose password screen
This commit is contained in:
parent
bbe1c9f554
commit
81f385bb11
9 changed files with 701 additions and 0 deletions
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum AuthenticationChoosePasswordViewModelResult {
|
||||
/// Submit with password and sign out of all devices option
|
||||
case submit(String, Bool)
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationChoosePasswordViewState: BindableState {
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationChoosePasswordBindings
|
||||
|
||||
/// Whether the password is valid and the user can continue.
|
||||
var hasInvalidPassword: Bool {
|
||||
bindings.password.count < 8
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationChoosePasswordBindings {
|
||||
/// The password input by the user.
|
||||
var password: String
|
||||
/// The signout all devices checkbox status
|
||||
var signoutAllDevices: Bool
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<AuthenticationChoosePasswordErrorType>?
|
||||
}
|
||||
|
||||
enum AuthenticationChoosePasswordViewAction {
|
||||
/// Send an email to the entered address.
|
||||
case submit
|
||||
/// Toggle sign out of all devices
|
||||
case toggleSignoutAllDevices
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
enum AuthenticationChoosePasswordErrorType: Hashable {
|
||||
/// An error response from the homeserver.
|
||||
case mxError(String)
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationChoosePasswordViewModelType = StateStoreViewModel<AuthenticationChoosePasswordViewState,
|
||||
Never,
|
||||
AuthenticationChoosePasswordViewAction>
|
||||
class AuthenticationChoosePasswordViewModel: AuthenticationChoosePasswordViewModelType, AuthenticationChoosePasswordViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (AuthenticationChoosePasswordViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(password: String = "", signoutAllDevices: Bool = false) {
|
||||
let viewState = AuthenticationChoosePasswordViewState(bindings: AuthenticationChoosePasswordBindings(password: password, signoutAllDevices: signoutAllDevices))
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: AuthenticationChoosePasswordViewAction) {
|
||||
switch viewAction {
|
||||
case .submit:
|
||||
Task { await callback?(.submit(state.bindings.password, state.bindings.signoutAllDevices)) }
|
||||
case .toggleSignoutAllDevices:
|
||||
state.bindings.signoutAllDevices.toggle()
|
||||
case .cancel:
|
||||
Task { await callback?(.cancel) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: AuthenticationChoosePasswordErrorType) {
|
||||
switch type {
|
||||
case .mxError(let message):
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: VectorL10n.error,
|
||||
message: message)
|
||||
case .unknown:
|
||||
state.bindings.alertInfo = AlertInfo(id: type)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationChoosePasswordViewModelProtocol {
|
||||
|
||||
var callback: (@MainActor (AuthenticationChoosePasswordViewModelResult) -> Void)? { get set }
|
||||
var context: AuthenticationChoosePasswordViewModelType.Context { get }
|
||||
|
||||
/// Display an error to the user.
|
||||
@MainActor func displayError(_ type: AuthenticationChoosePasswordErrorType)
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// Copyright 2021 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
|
||||
import CommonKit
|
||||
|
||||
struct AuthenticationChoosePasswordCoordinatorParameters {
|
||||
let loginWizard: LoginWizard
|
||||
}
|
||||
|
||||
enum AuthenticationChoosePasswordCoordinatorResult {
|
||||
/// Show the display name and/or avatar screens for the user to personalize their profile.
|
||||
case success
|
||||
/// Continue the flow by skipping the display name and avatar screens.
|
||||
case cancel
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class AuthenticationChoosePasswordCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: AuthenticationChoosePasswordCoordinatorParameters
|
||||
private let authenticationChoosePasswordHostingController: VectorHostingController
|
||||
private var authenticationChoosePasswordViewModel: AuthenticationChoosePasswordViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private var loginWizard: LoginWizard { parameters.loginWizard }
|
||||
|
||||
private var currentTask: Task<Void, Error>? {
|
||||
willSet {
|
||||
currentTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor (AuthenticationChoosePasswordCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: AuthenticationChoosePasswordCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AuthenticationChoosePasswordViewModel()
|
||||
let view = AuthenticationChoosePasswordScreen(viewModel: viewModel.context)
|
||||
authenticationChoosePasswordViewModel = viewModel
|
||||
authenticationChoosePasswordHostingController = VectorHostingController(rootView: view)
|
||||
authenticationChoosePasswordHostingController.vc_removeBackTitle()
|
||||
authenticationChoosePasswordHostingController.enableNavigationBarScrollEdgeAppearance = true
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationChoosePasswordHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[AuthenticationChoosePasswordCoordinator] did start.")
|
||||
Task { await setupViewModel() }
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.authenticationChoosePasswordHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`.
|
||||
@MainActor private func setupViewModel() {
|
||||
authenticationChoosePasswordViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[AuthenticationChoosePasswordCoordinator] AuthenticationChoosePasswordViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case .submit(let password, let signoutAllDevices):
|
||||
self.submitPassword(password, signoutAllDevices: signoutAllDevices)
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
@MainActor private func startLoading() {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Sends a validation email to the supplied address and then begins polling the server.
|
||||
@MainActor private func submitPassword(_ password: String, signoutAllDevices: Bool) {
|
||||
startLoading()
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
try await loginWizard.resetPasswordMailConfirmed(newPassword: password,
|
||||
signoutAllDevices: signoutAllDevices)
|
||||
|
||||
// Shouldn't be reachable but just in case, continue the flow.
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
self?.stopLoading()
|
||||
self?.callback?(.success)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
self?.stopLoading()
|
||||
self?.handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
@MainActor private func handleError(_ error: Error) {
|
||||
if let mxError = MXError(nsError: error as NSError) {
|
||||
authenticationChoosePasswordViewModel.displayError(.mxError(mxError.error))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle another other error types as needed.
|
||||
|
||||
authenticationChoosePasswordViewModel.displayError(.unknown)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// Copyright 2021 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.
|
||||
@available(iOS 14.0, *)
|
||||
enum MockAuthenticationChoosePasswordScreenState: MockScreenState, 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 enteredInvalidPassword
|
||||
case enteredValidPassword
|
||||
case enteredValidPasswordAndSignoutAllDevicesChecked
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
AuthenticationChoosePasswordScreen.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: AuthenticationChoosePasswordViewModel
|
||||
switch self {
|
||||
case .emptyPassword:
|
||||
viewModel = AuthenticationChoosePasswordViewModel()
|
||||
case .enteredInvalidPassword:
|
||||
viewModel = AuthenticationChoosePasswordViewModel(password: "1234")
|
||||
case .enteredValidPassword:
|
||||
viewModel = AuthenticationChoosePasswordViewModel(password: "12345678")
|
||||
case .enteredValidPasswordAndSignoutAllDevicesChecked:
|
||||
viewModel = AuthenticationChoosePasswordViewModel(password: "12345678", signoutAllDevices: true)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel], AnyView(AuthenticationChoosePasswordScreen(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationChoosePasswordScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationChoosePasswordUITests(selector: #selector(verifyAuthenticationChoosePasswordScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationChoosePasswordScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationChoosePasswordScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredInvalidPassword:
|
||||
verifyEnteredInvalidPassword()
|
||||
case .enteredValidPassword:
|
||||
verifyEnteredValidPassword()
|
||||
case .enteredValidPasswordAndSignoutAllDevicesChecked:
|
||||
verifyEnteredValidPasswordAndSignoutAllDevicesChecked()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
let passwordTextField = app.textFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.value as? String, "New Password", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let submitButton = app.buttons["submitButton"]
|
||||
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
|
||||
XCTAssertFalse(submitButton.isEnabled, "The submit button should be disabled before text is input.")
|
||||
|
||||
let doNotSignoutAllDevicesImage = app.images["doNotSignoutAllDevices"]
|
||||
XCTAssertTrue(doNotSignoutAllDevicesImage.exists, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredInvalidPassword() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
let passwordTextField = app.textFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.value as? String, "1234", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let submitButton = app.buttons["submitButton"]
|
||||
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
|
||||
XCTAssertFalse(submitButton.isEnabled, "The submit button should be disabled when password is invalid.")
|
||||
|
||||
let doNotSignoutAllDevicesImage = app.images["doNotSignoutAllDevices"]
|
||||
XCTAssertTrue(doNotSignoutAllDevicesImage.exists, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPassword() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
let passwordTextField = app.textFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.value as? String, "12345678", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let submitButton = app.buttons["submitButton"]
|
||||
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
|
||||
XCTAssertTrue(submitButton.isEnabled, "The submit button should be enabled after password is valid.")
|
||||
|
||||
let doNotSignoutAllDevicesImage = app.images["doNotSignoutAllDevices"]
|
||||
XCTAssertTrue(doNotSignoutAllDevicesImage.exists, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPasswordAndSignoutAllDevicesChecked() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
let passwordTextField = app.textFields["passwordTextField"]
|
||||
XCTAssertTrue(passwordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(passwordTextField.value as? String, "12345678", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let submitButton = app.buttons["submitButton"]
|
||||
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
|
||||
XCTAssertTrue(submitButton.isEnabled, "The submit button should be enabled after password is valid.")
|
||||
|
||||
let signoutAllDevicesImage = app.images["signoutAllDevices"]
|
||||
XCTAssertTrue(signoutAllDevicesImage.exists, "Sign out all devices should be checked")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// Copyright 2021 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 XCTest
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class AuthenticationChoosePasswordViewModelTests: XCTestCase {
|
||||
|
||||
@MainActor func testInitialState() async {
|
||||
let viewModel = AuthenticationChoosePasswordViewModel()
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.viewState.bindings.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssert(context.viewState.hasInvalidPassword, "The view model should start with an invalid password.")
|
||||
XCTAssertFalse(context.signoutAllDevices, "The view model should start with sign out of all devices unchecked.")
|
||||
}
|
||||
|
||||
@MainActor func testToggleSignoutAllDevices() async {
|
||||
|
||||
let viewModel = AuthenticationChoosePasswordViewModel()
|
||||
let context = viewModel.context
|
||||
|
||||
viewModel.process(viewAction: .toggleSignoutAllDevices)
|
||||
|
||||
XCTAssertTrue(context.signoutAllDevices, "The view model should update signoutAllDevices.")
|
||||
|
||||
viewModel.process(viewAction: .toggleSignoutAllDevices)
|
||||
|
||||
XCTAssertFalse(context.signoutAllDevices, "The view model should update signoutAllDevices again.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// The form shown to enter an email address.
|
||||
struct AuthenticationChoosePasswordForm: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
@State private var isEditingTextField = false
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: AuthenticationChoosePasswordViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
|
||||
mainContent
|
||||
}
|
||||
}
|
||||
|
||||
/// The title, message and icon at the top of the screen.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
OnboardingIconImage(image: Asset.Images.authenticationEmailIcon)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(VectorL10n.authenticationChoosePasswordInputTitle)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(VectorL10n.authenticationChoosePasswordInputMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.accessibilityIdentifier("messageLabel")
|
||||
}
|
||||
}
|
||||
|
||||
/// The text field and submit button where the user enters an email address.
|
||||
var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if #available(iOS 15.0, *) {
|
||||
textField
|
||||
.onSubmit(submit)
|
||||
} else {
|
||||
textField
|
||||
}
|
||||
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
signoutAllDevicesImage
|
||||
.foregroundColor(theme.colors.accent)
|
||||
Text(VectorL10n.authenticationChoosePasswordSignoutAllDevices)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
.onTapGesture(perform: toggleSignoutAllDevices)
|
||||
|
||||
Button(action: submit) {
|
||||
Text(VectorL10n.authenticationChoosePasswordSubmitButton)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(viewModel.viewState.hasInvalidPassword)
|
||||
.accessibilityIdentifier("submitButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// The text field, extracted for iOS 15 modifiers to be applied.
|
||||
var textField: some View {
|
||||
TextField(VectorL10n.authenticationChoosePasswordTextFieldPlaceholder, text: $viewModel.password) {
|
||||
isEditingTextField = $0
|
||||
}
|
||||
.textFieldStyle(BorderedInputFieldStyle(isEditing: isEditingTextField, isError: false))
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.accessibilityIdentifier("passwordTextField")
|
||||
}
|
||||
|
||||
var signoutAllDevicesImage: some View {
|
||||
if viewModel.signoutAllDevices {
|
||||
return Image(uiImage: Asset.Images.selectionTick.image)
|
||||
.accessibilityIdentifier("signoutAllDevices")
|
||||
} else {
|
||||
return Image(uiImage: Asset.Images.selectionUntick.image)
|
||||
.accessibilityIdentifier("doNotSignoutAllDevices")
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the `send` view action so long as a valid email address has been input.
|
||||
func submit() {
|
||||
guard !viewModel.viewState.hasInvalidPassword else { return }
|
||||
viewModel.send(viewAction: .submit)
|
||||
}
|
||||
|
||||
/// Sends the `toggleSignoutAllDevices` view action.
|
||||
func toggleSignoutAllDevices() {
|
||||
viewModel.send(viewAction: .toggleSignoutAllDevices)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright 2021 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 AuthenticationChoosePasswordScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: AuthenticationChoosePasswordViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
ScrollView {
|
||||
mainContent
|
||||
.readableFrame()
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var mainContent: some View {
|
||||
AuthenticationChoosePasswordForm(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationChoosePasswordScreen_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockAuthenticationChoosePasswordScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
.theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue