mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Create change password screen
This commit is contained in:
parent
54b7333665
commit
1778608d95
9 changed files with 685 additions and 0 deletions
|
@ -51,6 +51,7 @@ enum MockAppScreens {
|
|||
MockSpaceCreationSettingsScreenState.self,
|
||||
MockSpaceCreationPostProcessScreenState.self,
|
||||
MockTimelinePollScreenState.self,
|
||||
MockChangePasswordScreenState.self,
|
||||
MockTemplateSimpleScreenScreenState.self,
|
||||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
|
|
|
@ -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
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum ChangePasswordViewModelResult {
|
||||
/// Submit with old and new passwords and sign out of all devices option
|
||||
case submit(String, String, Bool)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct ChangePasswordViewState: BindableState {
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: ChangePasswordBindings
|
||||
|
||||
/// Whether the user can submit the form: old password should be entered, and new passwords should match
|
||||
var canSubmit: Bool {
|
||||
!bindings.oldPassword.isEmpty
|
||||
&& !bindings.newPassword1.isEmpty
|
||||
&& bindings.newPassword1 == bindings.newPassword2
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangePasswordBindings {
|
||||
/// The password input by the user.
|
||||
var oldPassword: String
|
||||
/// The new password input by the user.
|
||||
var newPassword1: String
|
||||
/// The new password confirmation input by the user.
|
||||
var newPassword2: String
|
||||
/// The signout all devices checkbox status
|
||||
var signoutAllDevices: Bool
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<ChangePasswordErrorType>?
|
||||
}
|
||||
|
||||
enum ChangePasswordViewAction {
|
||||
/// Send an email to the entered address.
|
||||
case submit
|
||||
/// Toggle sign out of all devices
|
||||
case toggleSignoutAllDevices
|
||||
}
|
||||
|
||||
enum ChangePasswordErrorType: Hashable {
|
||||
/// An error response from the homeserver.
|
||||
case mxError(String)
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// 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 ChangePasswordViewModelType = StateStoreViewModel<ChangePasswordViewState,
|
||||
Never,
|
||||
ChangePasswordViewAction>
|
||||
class ChangePasswordViewModel: ChangePasswordViewModelType, ChangePasswordViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: (@MainActor (ChangePasswordViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(oldPassword: String = "",
|
||||
newPassword1: String = "",
|
||||
newPassword2: String = "",
|
||||
signoutAllDevices: Bool = true) {
|
||||
let bindings = ChangePasswordBindings(oldPassword: oldPassword,
|
||||
newPassword1: newPassword1,
|
||||
newPassword2: newPassword2,
|
||||
signoutAllDevices: signoutAllDevices)
|
||||
let viewState = ChangePasswordViewState(bindings: bindings)
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: ChangePasswordViewAction) {
|
||||
switch viewAction {
|
||||
case .submit:
|
||||
Task { await callback?(.submit(state.bindings.oldPassword,
|
||||
state.bindings.newPassword1,
|
||||
state.bindings.signoutAllDevices)) }
|
||||
case .toggleSignoutAllDevices:
|
||||
state.bindings.signoutAllDevices.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: ChangePasswordErrorType) {
|
||||
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 ChangePasswordViewModelProtocol {
|
||||
|
||||
var callback: (@MainActor (ChangePasswordViewModelResult) -> Void)? { get set }
|
||||
var context: ChangePasswordViewModelType.Context { get }
|
||||
|
||||
/// Display an error to the user.
|
||||
@MainActor func displayError(_ type: ChangePasswordErrorType)
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// 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 ChangePasswordCoordinatorParameters {
|
||||
let restClient: MXRestClient
|
||||
}
|
||||
|
||||
final class ChangePasswordCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: ChangePasswordCoordinatorParameters
|
||||
private let changePasswordHostingController: VectorHostingController
|
||||
private var changePasswordViewModel: ChangePasswordViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
private var currentTask: Task<Void, Error>? {
|
||||
willSet {
|
||||
currentTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: (@MainActor () -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: ChangePasswordCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = ChangePasswordViewModel()
|
||||
let view = ChangePasswordScreen(viewModel: viewModel.context)
|
||||
changePasswordViewModel = viewModel
|
||||
changePasswordHostingController = VectorHostingController(rootView: view)
|
||||
changePasswordHostingController.vc_removeBackTitle()
|
||||
changePasswordHostingController.enableNavigationBarScrollEdgeAppearance = true
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: changePasswordHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[ChangePasswordCoordinator] did start.")
|
||||
Task { await setupViewModel() }
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.changePasswordHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`.
|
||||
@MainActor private func setupViewModel() {
|
||||
changePasswordViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[ChangePasswordCoordinator] ChangePasswordViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case .submit(let oldPassword, let newPassword, let signoutAllDevices):
|
||||
self.changePassword(from: oldPassword, to: newPassword, signoutAllDevices: signoutAllDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Submits a reset password request with signing out of all devices option
|
||||
@MainActor private func changePassword(from oldPassword: String, to newPassword: String, signoutAllDevices: Bool) {
|
||||
startLoading()
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
try await parameters.restClient.changePassword(from: oldPassword, to: newPassword, logoutDevices: signoutAllDevices)
|
||||
|
||||
// Shouldn't be reachable but just in case, continue the flow.
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
self?.stopLoading()
|
||||
self?.callback?()
|
||||
} 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) {
|
||||
changePasswordViewModel.displayError(.mxError(mxError.error))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle another other error types as needed.
|
||||
|
||||
changePasswordViewModel.displayError(.unknown)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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 MockChangePasswordScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case allEmpty
|
||||
case cannotSubmit
|
||||
case canSubmit
|
||||
case canSubmitAndSignoutAllDevicesChecked
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
ChangePasswordScreen.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: ChangePasswordViewModel
|
||||
switch self {
|
||||
case .allEmpty:
|
||||
viewModel = ChangePasswordViewModel()
|
||||
case .cannotSubmit:
|
||||
viewModel = ChangePasswordViewModel(oldPassword: "12345678",
|
||||
newPassword1: "87654321",
|
||||
newPassword2: "97654321")
|
||||
case .canSubmit:
|
||||
viewModel = ChangePasswordViewModel(oldPassword: "12345678",
|
||||
newPassword1: "87654321",
|
||||
newPassword2: "87654321")
|
||||
case .canSubmitAndSignoutAllDevicesChecked:
|
||||
viewModel = ChangePasswordViewModel(oldPassword: "12345678",
|
||||
newPassword1: "87654321",
|
||||
newPassword2: "87654321",
|
||||
signoutAllDevices: true)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel], AnyView(ChangePasswordScreen(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// 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 ChangePasswordUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockChangePasswordScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return ChangePasswordUITests(selector: #selector(verifyChangePasswordScreen))
|
||||
}
|
||||
|
||||
func verifyChangePasswordScreen() throws {
|
||||
guard let screenState = screenState as? MockChangePasswordScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .allEmpty:
|
||||
verifyAllEmpty()
|
||||
case .cannotSubmit:
|
||||
verifyCannotSubmit()
|
||||
case .canSubmit:
|
||||
verifyCanSubmit()
|
||||
case .canSubmitAndSignoutAllDevicesChecked:
|
||||
verifyCanSubmitAndSignoutAllDevicesChecked()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyAllEmpty() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
|
||||
let oldPasswordTextField = app.secureTextFields["oldPasswordTextField"]
|
||||
XCTAssertTrue(oldPasswordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(oldPasswordTextField.label, "old password", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField1 = app.secureTextFields["newPasswordTextField1"]
|
||||
XCTAssertTrue(newPasswordTextField1.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField1.label, "new password", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
|
||||
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField2.label, "confirm 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 when not able to submit.")
|
||||
|
||||
let signoutAllDevicesToggle = app.switches["signoutAllDevicesToggle"]
|
||||
XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist")
|
||||
XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked")
|
||||
}
|
||||
|
||||
func verifyCannotSubmit() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
|
||||
let oldPasswordTextField = app.secureTextFields["oldPasswordTextField"]
|
||||
XCTAssertTrue(oldPasswordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(oldPasswordTextField.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField1 = app.secureTextFields["newPasswordTextField1"]
|
||||
XCTAssertTrue(newPasswordTextField1.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField1.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
|
||||
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField2.value as? String, "••••••••", "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 not able to submit.")
|
||||
|
||||
let signoutAllDevicesToggle = app.switches["signoutAllDevicesToggle"]
|
||||
XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist")
|
||||
XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked")
|
||||
}
|
||||
|
||||
func verifyCanSubmit() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
|
||||
let oldPasswordTextField = app.secureTextFields["oldPasswordTextField"]
|
||||
XCTAssertTrue(oldPasswordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(oldPasswordTextField.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField1 = app.secureTextFields["newPasswordTextField1"]
|
||||
XCTAssertTrue(newPasswordTextField1.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField1.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
|
||||
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField2.value as? String, "••••••••", "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 when able to submit.")
|
||||
|
||||
let signoutAllDevicesToggle = app.switches["signoutAllDevicesToggle"]
|
||||
XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist")
|
||||
XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked")
|
||||
}
|
||||
|
||||
func verifyCanSubmitAndSignoutAllDevicesChecked() {
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
|
||||
let oldPasswordTextField = app.secureTextFields["oldPasswordTextField"]
|
||||
XCTAssertTrue(oldPasswordTextField.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(oldPasswordTextField.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField1 = app.secureTextFields["newPasswordTextField1"]
|
||||
XCTAssertTrue(newPasswordTextField1.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField1.value as? String, "••••••••", "The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
|
||||
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
|
||||
XCTAssertEqual(newPasswordTextField2.value as? String, "••••••••", "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 when able to submit.")
|
||||
|
||||
let signoutAllDevicesToggle = app.switches["signoutAllDevicesToggle"]
|
||||
XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist")
|
||||
XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// 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 ChangePasswordViewModelTests: XCTestCase {
|
||||
|
||||
@MainActor func testEmptyState() async {
|
||||
let viewModel = ChangePasswordViewModel()
|
||||
let context = viewModel.context
|
||||
|
||||
// Given an empty view model
|
||||
XCTAssert(context.oldPassword.isEmpty, "The view model should start with an empty old password.")
|
||||
XCTAssert(context.newPassword1.isEmpty, "The view model should start with an empty new password 1.")
|
||||
XCTAssert(context.newPassword2.isEmpty, "The view model should start with an empty new password 2.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The view model should not be able to submit.")
|
||||
XCTAssertTrue(context.signoutAllDevices, "The view model should start with sign out of all devices checked.")
|
||||
}
|
||||
|
||||
@MainActor func testValidState() async {
|
||||
let viewModel = ChangePasswordViewModel(oldPassword: "12345678",
|
||||
newPassword1: "87654321",
|
||||
newPassword2: "87654321",
|
||||
signoutAllDevices: false)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a filled view model in valid state
|
||||
XCTAssertFalse(context.oldPassword.isEmpty, "The view model should start with an empty old password.")
|
||||
XCTAssertFalse(context.newPassword1.isEmpty, "The view model should start with an empty new password 1.")
|
||||
XCTAssertFalse(context.newPassword2.isEmpty, "The view model should start with an empty new password 2.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The view model should be able to submit.")
|
||||
XCTAssertFalse(context.signoutAllDevices, "Sign out of all devices should be unchecked.")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
//
|
||||
// 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 ChangePasswordScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
enum Field { case oldPassword, newPassword1, newPassword2 }
|
||||
@State private var focusedField: Field?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: ChangePasswordViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 36)
|
||||
form
|
||||
}
|
||||
.readableFrame()
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
/// The title and icon at the top of the screen.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
OnboardingIconImage(image: Asset.Images.authenticationPasswordIcon)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Text(VectorL10n.settingsChangePassword)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
}
|
||||
}
|
||||
|
||||
/// The text fields and submit button.
|
||||
var form: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.settingsOldPassword,
|
||||
text: $viewModel.oldPassword,
|
||||
isFirstResponder: focusedField == .oldPassword,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .next,
|
||||
isSecureTextEntry: true),
|
||||
onCommit: { focusedField = .newPassword1 })
|
||||
.accessibilityIdentifier("oldPasswordTextField")
|
||||
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.settingsNewPassword,
|
||||
text: $viewModel.newPassword1,
|
||||
isFirstResponder: focusedField == .newPassword1,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .next,
|
||||
isSecureTextEntry: true),
|
||||
onCommit: { focusedField = .newPassword2 })
|
||||
.accessibilityIdentifier("newPasswordTextField1")
|
||||
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.settingsConfirmPassword,
|
||||
text: $viewModel.newPassword2,
|
||||
isFirstResponder: focusedField == .newPassword2,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .done,
|
||||
isSecureTextEntry: true),
|
||||
onCommit: submit)
|
||||
.accessibilityIdentifier("newPasswordTextField2")
|
||||
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
Toggle(VectorL10n.authenticationChoosePasswordSignoutAllDevices, isOn: $viewModel.signoutAllDevices)
|
||||
.toggleStyle(AuthenticationTermsToggleStyle())
|
||||
.accessibilityIdentifier("signoutAllDevicesToggle")
|
||||
Text(VectorL10n.authenticationChoosePasswordSignoutAllDevices)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 16)
|
||||
.onTapGesture(perform: toggleSignoutAllDevices)
|
||||
|
||||
Button(action: submit) {
|
||||
Text(VectorL10n.save)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.canSubmit)
|
||||
.accessibilityIdentifier("submitButton")
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the `submit` view action if viewModel.viewState.canSubmit.
|
||||
func submit() {
|
||||
guard viewModel.viewState.canSubmit else { return }
|
||||
viewModel.send(viewAction: .submit)
|
||||
}
|
||||
|
||||
/// Sends the `toggleSignoutAllDevices` view action.
|
||||
func toggleSignoutAllDevices() {
|
||||
viewModel.send(viewAction: .toggleSignoutAllDevices)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ChangePasswordScreen_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockChangePasswordScreenState.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