mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Add registration terms screen. (#6128)
Begin implementing Auth Terms coordinator.
This commit is contained in:
parent
4a9ace0cf3
commit
1b160b5f25
19 changed files with 722 additions and 4 deletions
15
Riot/Assets/Images.xcassets/Authentication/authentication_terms_icon.imageset/Contents.json
vendored
Normal file
15
Riot/Assets/Images.xcassets/Authentication/authentication_terms_icon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "authentication_terms_icon.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 70 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M35,70C54.33,70 70,54.33 70,35C70,15.67 54.33,0 35,0C15.67,0 0,15.67 0,35C0,54.33 15.67,70 35,70ZM18.778,21.722C18.778,18.501 21.39,15.889 24.611,15.889L44.056,15.889C47.277,15.889 49.889,18.501 49.889,21.722L49.889,48.945C49.889,52.166 47.277,54.778 44.056,54.778L24.611,54.778C21.39,54.778 18.778,52.166 18.778,48.945L18.778,21.722ZM26.07,45.056C25.264,45.056 24.611,45.709 24.611,46.514C24.611,47.319 25.264,47.972 26.07,47.972L34.82,47.972C35.625,47.972 36.278,47.319 36.278,46.514C36.278,45.709 35.625,45.056 34.82,45.056L26.07,45.056ZM24.611,39.708C24.611,38.903 25.264,38.25 26.07,38.25L42.597,38.25C43.403,38.25 44.056,38.903 44.056,39.708C44.056,40.514 43.403,41.167 42.597,41.167L26.07,41.167C25.264,41.167 24.611,40.514 24.611,39.708Z" style="fill:rgb(13,189,139);"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -49,6 +49,9 @@
|
|||
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_verify_email_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_terms_title" = "Privacy policy";
|
||||
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
|
||||
|
||||
// MARK: Spaces WIP
|
||||
"spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer.";
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ internal class Asset: NSObject {
|
|||
internal static let authenticationSsoIconGitlab = ImageAsset(name: "authentication_sso_icon_gitlab")
|
||||
internal static let authenticationSsoIconGoogle = ImageAsset(name: "authentication_sso_icon_google")
|
||||
internal static let authenticationSsoIconTwitter = ImageAsset(name: "authentication_sso_icon_twitter")
|
||||
internal static let authenticationTermsIcon = ImageAsset(name: "authentication_terms_icon")
|
||||
internal static let callAudioMuteOffIcon = ImageAsset(name: "call_audio_mute_off_icon")
|
||||
internal static let callAudioMuteOnIcon = ImageAsset(name: "call_audio_mute_on_icon")
|
||||
internal static let callAudioRouteBuiltin = ImageAsset(name: "call_audio_route_builtin")
|
||||
|
|
|
@ -78,6 +78,14 @@ public extension VectorL10n {
|
|||
static var authenticationServerSelectionTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_title")
|
||||
}
|
||||
/// Please read through T&C. You must accept in order to continue.
|
||||
static var authenticationTermsMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_message")
|
||||
}
|
||||
/// Privacy policy
|
||||
static var authenticationTermsTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_title")
|
||||
}
|
||||
/// This will help verify your account and enables password recovery.
|
||||
static var authenticationVerifyEmailInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
|
||||
|
|
|
@ -134,7 +134,7 @@ struct FlowResult {
|
|||
case dummy(mandatory: Bool)
|
||||
|
||||
/// The stage with the type `m.login.terms`.
|
||||
case terms(mandatory: Bool, policies: [String: String])
|
||||
case terms(mandatory: Bool, policies: [AnyHashable: Any])
|
||||
|
||||
/// A stage of an unknown type.
|
||||
case other(mandatory: Bool, type: String, params: [AnyHashable: Any])
|
||||
|
@ -166,7 +166,7 @@ extension MXAuthenticationSession {
|
|||
case kMXLoginFlowTypeDummy:
|
||||
stage = .dummy(mandatory: isMandatory)
|
||||
case kMXLoginFlowTypeTerms:
|
||||
let parameters = params[flow] as? [String: String]
|
||||
let parameters = params[flow] as? [AnyHashable: Any]
|
||||
stage = .terms(mandatory: isMandatory, policies: parameters ?? [:])
|
||||
case kMXLoginFlowTypeMSISDN:
|
||||
stage = .msisdn(mandatory: isMandatory)
|
||||
|
|
|
@ -39,7 +39,9 @@ struct AuthenticationRegistrationScreen: View {
|
|||
serverInfo
|
||||
.padding(.leading, 12)
|
||||
|
||||
Divider()
|
||||
Rectangle()
|
||||
.fill(theme.colors.quinaryContent)
|
||||
.frame(height: 1)
|
||||
.padding(.vertical, 21)
|
||||
|
||||
if viewModel.viewState.showRegistrationForm {
|
||||
|
@ -63,9 +65,9 @@ struct AuthenticationRegistrationScreen: View {
|
|||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.accentColor(theme.colors.accent)
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
/// The header containing the icon, title and message.
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
struct AuthenticationTermsPolicy: Identifiable, Equatable {
|
||||
var id: String { url }
|
||||
/// The URL that can be used to view the policy.
|
||||
let url: String
|
||||
/// The policy's title.
|
||||
let title: String
|
||||
/// The policy's description.
|
||||
let description: String
|
||||
/// Whether or not the policy has been accepted.
|
||||
var accepted: Bool = false
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum AuthenticationTermsViewModelResult {
|
||||
/// Continue on to the next step in the flow.
|
||||
case next
|
||||
/// Show the selected policy.
|
||||
case showPolicy(AuthenticationTermsPolicy)
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationTermsViewState: BindableState {
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationTermsBindings
|
||||
|
||||
/// Whether or not all of the policies have been accepted.
|
||||
var hasAcceptedAllPolicies: Bool {
|
||||
bindings.policies.allSatisfy(\.accepted)
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationTermsBindings {
|
||||
/// All of the policies that need to be accepted.
|
||||
var policies: [AuthenticationTermsPolicy]
|
||||
/// Information about the currently displayed alert.
|
||||
var alertInfo: AlertInfo<AuthenticationTermsErrorType>?
|
||||
}
|
||||
|
||||
enum AuthenticationTermsViewAction {
|
||||
/// Continue on to the next step in the flow.
|
||||
case next
|
||||
/// Show the selected policy.
|
||||
case showPolicy(AuthenticationTermsPolicy)
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
enum AuthenticationTermsErrorType: Hashable {
|
||||
/// An error response from the homeserver.
|
||||
case mxError(String)
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
typealias AuthenticationTermsViewModelType = StateStoreViewModel<AuthenticationTermsViewState,
|
||||
Never,
|
||||
AuthenticationTermsViewAction>
|
||||
|
||||
class AuthenticationTermsViewModel: AuthenticationTermsViewModelType, AuthenticationTermsViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@MainActor var callback: ((AuthenticationTermsViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(policies: [AuthenticationTermsPolicy]) {
|
||||
super.init(initialViewState: AuthenticationTermsViewState(bindings: AuthenticationTermsBindings(policies: policies)))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: AuthenticationTermsViewAction) {
|
||||
switch viewAction {
|
||||
case .next:
|
||||
Task { await callback?(.next) }
|
||||
case .showPolicy(let policy):
|
||||
Task { await callback?(.showPolicy(policy)) }
|
||||
case .cancel:
|
||||
Task { await callback?(.cancel) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: AuthenticationTermsErrorType) {
|
||||
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 AuthenticationTermsViewModelProtocol {
|
||||
|
||||
@MainActor var callback: ((AuthenticationTermsViewModelResult) -> Void)? { get set }
|
||||
var context: AuthenticationTermsViewModelType.Context { get }
|
||||
|
||||
/// Display an error to the user.
|
||||
@MainActor func displayError(_ type: AuthenticationTermsErrorType)
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// 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 AuthenticationTermsCoordinatorParameters {
|
||||
let registrationWizard: RegistrationWizard
|
||||
/// The policies to be accepted by the user.
|
||||
let policies: [String: String]
|
||||
/// The address of the homeserver (shown beneath the policies).
|
||||
let homeserverAddress: String
|
||||
}
|
||||
|
||||
enum AuthenticationTermsCoordinatorResult {
|
||||
/// The screen completed with the associated registration result.
|
||||
case completed(RegistrationResult)
|
||||
/// The user would like to cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class AuthenticationTermsCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: AuthenticationTermsCoordinatorParameters
|
||||
private let authenticationTermsHostingController: UIViewController
|
||||
private var authenticationTermsViewModel: AuthenticationTermsViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
var registrationWizard: RegistrationWizard { parameters.registrationWizard }
|
||||
|
||||
private var currentTask: Task<Void, Error>? {
|
||||
willSet {
|
||||
currentTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
@MainActor var callback: ((AuthenticationTermsCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: AuthenticationTermsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let policies = parameters.policies.map { AuthenticationTermsPolicy(url: $0.value, title: $0.key, description: parameters.homeserverAddress) }
|
||||
let viewModel = AuthenticationTermsViewModel(policies: policies)
|
||||
let view = AuthenticationTermsScreen(viewModel: viewModel.context)
|
||||
authenticationTermsViewModel = viewModel
|
||||
authenticationTermsHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationTermsHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[AuthenticationTermsCoordinator] did start.")
|
||||
Task { await setupViewModel() }
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.authenticationTermsHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`.
|
||||
@MainActor private func setupViewModel() {
|
||||
authenticationTermsViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[AuthenticationTermsCoordinator] AuthenticationTermsViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case .next:
|
||||
self.acceptTerms()
|
||||
case .showPolicy(let policy):
|
||||
self.show(policy)
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
@MainActor private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
/// Accept all of the policies and continue.
|
||||
@MainActor private func acceptTerms() {
|
||||
startLoading()
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
let result = try await registrationWizard.acceptTerms()
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
callback?(.completed(result))
|
||||
|
||||
self?.stopLoading()
|
||||
} catch {
|
||||
handleError(error)
|
||||
self?.stopLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Present the policy in a modal.
|
||||
@MainActor private func show(_ policy: AuthenticationTermsPolicy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
authenticationTermsViewModel.displayError(.mxError(mxError.error))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle any other error types as needed.
|
||||
|
||||
authenticationTermsViewModel.displayError(.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 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 MockAuthenticationTermsScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case matrixDotOrg
|
||||
case accepted
|
||||
case multiple
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
AuthenticationTermsScreen.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: AuthenticationTermsViewModel
|
||||
switch self {
|
||||
case .matrixDotOrg:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
description: "matrix.org")])
|
||||
case .accepted:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
description: "matrix.org",
|
||||
accepted: true)])
|
||||
case .multiple:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [
|
||||
AuthenticationTermsPolicy(url: "https://example.com/terms", title: "Terms and Conditions", description: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/privacy", title: "Privacy Policy", description: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/conduct", title: "Code of Conduct", description: "example.com")
|
||||
])
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel], AnyView(AuthenticationTermsScreen(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// 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 AuthenticationTermsUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationTermsScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationTermsUITests(selector: #selector(verifyAuthenticationTermsScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationTermsScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationTermsScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
verifyTerms(accepted: false)
|
||||
case .accepted:
|
||||
verifyTerms(accepted: true)
|
||||
case .multiple:
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTerms(accepted: Bool) {
|
||||
let nextButton = app.buttons["nextButton"]
|
||||
XCTAssertTrue(nextButton.exists, "The next button should always exist.")
|
||||
XCTAssertEqual(nextButton.isEnabled, accepted, "The next button should be enabled when the terms are accepted")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// 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 AuthenticationTermsViewModelTests: XCTestCase {
|
||||
// Nothing to test as the checkbox binds to the model directly, the view model never mutates the model
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// 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 AuthenticationTermsListItem: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@Binding var policy: AuthenticationTermsPolicy
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
label
|
||||
.padding(.vertical, 14)
|
||||
.overlay(separator, alignment: .bottom)
|
||||
}
|
||||
.buttonStyle(FormItemButtonStyle())
|
||||
}
|
||||
|
||||
/// The content to be shown in the list row.
|
||||
var label: some View {
|
||||
HStack(spacing: 16) {
|
||||
Toggle(VectorL10n.accept, isOn: $policy.accepted)
|
||||
.toggleStyle(AuthenticationTermsToggleStyle())
|
||||
.accessibilityLabel(VectorL10n.accept)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(policy.title)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(policy.description)
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
/// The separator shown beneath the item.
|
||||
var separator: some View {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(theme.colors.system)
|
||||
.padding(.leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct Previews_AuthenticationTermsListItem_Previews: PreviewProvider {
|
||||
static var unaccepted = AuthenticationTermsPolicy(url: "",
|
||||
title: "Terms and Conditions",
|
||||
description: "matrix.org")
|
||||
static var accepted = AuthenticationTermsPolicy(url: "",
|
||||
title: "Terms and Conditions",
|
||||
description: "matrix.org",
|
||||
accepted: true)
|
||||
static var previews: some View {
|
||||
VStack(spacing: 0) {
|
||||
AuthenticationTermsListItem(policy: .constant(unaccepted)) { }
|
||||
AuthenticationTermsListItem(policy: .constant(accepted)) { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// 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 AuthenticationTermsScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: AuthenticationTermsViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 40) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.horizontal)
|
||||
|
||||
termsList // No horizontal padding as the list should span edge-to-edge.
|
||||
|
||||
button
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: OnboardingMetrics.maxContentWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
/// The header containing the icon, title and message.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
OnboardingIconImage(image: Asset.Images.authenticationTermsIcon)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(VectorL10n.authenticationTermsTitle)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationTermsMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of polices to be accepted.
|
||||
var termsList: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach($viewModel.policies) { $policy in
|
||||
AuthenticationTermsListItem(policy: $policy) {
|
||||
viewModel.send(viewAction: .showPolicy(policy))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The action button shown below the list.
|
||||
var button: some View {
|
||||
VStack {
|
||||
Button { viewModel.send(viewAction: .next) } label: {
|
||||
Text(VectorL10n.next)
|
||||
.font(theme.fonts.body)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.hasAcceptedAllPolicies)
|
||||
.accessibilityIdentifier("nextButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationTerms_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockAuthenticationTermsScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// 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 toggle style that uses a button, with a checked/unchecked image like a checkbox.
|
||||
struct AuthenticationTermsToggleStyle: ToggleStyle {
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
Button { configuration.isOn.toggle() } label: {
|
||||
Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square")
|
||||
.font(.title3.weight(.regular))
|
||||
.imageScale(.large)
|
||||
.foregroundColor(configuration.isOn ? theme.colors.accent : theme.colors.tertiaryContent)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import Foundation
|
|||
enum MockAppScreens {
|
||||
static let appScreens: [MockScreenState.Type] = [
|
||||
MockLiveLocationSharingViewerScreenState.self,
|
||||
MockAuthenticationTermsScreenState.self,
|
||||
MockAuthenticationVerifyEmailScreenState.self,
|
||||
MockAuthenticationRegistrationScreenState.self,
|
||||
MockAuthenticationServerSelectionScreenState.self,
|
||||
|
|
1
changelog.d/5650.wip
Normal file
1
changelog.d/5650.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Authentication: Create terms screen.
|
Loading…
Reference in a new issue