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_hint" = "Did not receive an email?";
|
||||||
"authentication_verify_email_waiting_button" = "Resend 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
|
// MARK: Spaces WIP
|
||||||
"spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer.";
|
"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 authenticationSsoIconGitlab = ImageAsset(name: "authentication_sso_icon_gitlab")
|
||||||
internal static let authenticationSsoIconGoogle = ImageAsset(name: "authentication_sso_icon_google")
|
internal static let authenticationSsoIconGoogle = ImageAsset(name: "authentication_sso_icon_google")
|
||||||
internal static let authenticationSsoIconTwitter = ImageAsset(name: "authentication_sso_icon_twitter")
|
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 callAudioMuteOffIcon = ImageAsset(name: "call_audio_mute_off_icon")
|
||||||
internal static let callAudioMuteOnIcon = ImageAsset(name: "call_audio_mute_on_icon")
|
internal static let callAudioMuteOnIcon = ImageAsset(name: "call_audio_mute_on_icon")
|
||||||
internal static let callAudioRouteBuiltin = ImageAsset(name: "call_audio_route_builtin")
|
internal static let callAudioRouteBuiltin = ImageAsset(name: "call_audio_route_builtin")
|
||||||
|
|
|
@ -78,6 +78,14 @@ public extension VectorL10n {
|
||||||
static var authenticationServerSelectionTitle: String {
|
static var authenticationServerSelectionTitle: String {
|
||||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_title")
|
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.
|
/// This will help verify your account and enables password recovery.
|
||||||
static var authenticationVerifyEmailInputMessage: String {
|
static var authenticationVerifyEmailInputMessage: String {
|
||||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
|
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
|
||||||
|
|
|
@ -134,7 +134,7 @@ struct FlowResult {
|
||||||
case dummy(mandatory: Bool)
|
case dummy(mandatory: Bool)
|
||||||
|
|
||||||
/// The stage with the type `m.login.terms`.
|
/// 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.
|
/// A stage of an unknown type.
|
||||||
case other(mandatory: Bool, type: String, params: [AnyHashable: Any])
|
case other(mandatory: Bool, type: String, params: [AnyHashable: Any])
|
||||||
|
@ -166,7 +166,7 @@ extension MXAuthenticationSession {
|
||||||
case kMXLoginFlowTypeDummy:
|
case kMXLoginFlowTypeDummy:
|
||||||
stage = .dummy(mandatory: isMandatory)
|
stage = .dummy(mandatory: isMandatory)
|
||||||
case kMXLoginFlowTypeTerms:
|
case kMXLoginFlowTypeTerms:
|
||||||
let parameters = params[flow] as? [String: String]
|
let parameters = params[flow] as? [AnyHashable: Any]
|
||||||
stage = .terms(mandatory: isMandatory, policies: parameters ?? [:])
|
stage = .terms(mandatory: isMandatory, policies: parameters ?? [:])
|
||||||
case kMXLoginFlowTypeMSISDN:
|
case kMXLoginFlowTypeMSISDN:
|
||||||
stage = .msisdn(mandatory: isMandatory)
|
stage = .msisdn(mandatory: isMandatory)
|
||||||
|
|
|
@ -39,7 +39,9 @@ struct AuthenticationRegistrationScreen: View {
|
||||||
serverInfo
|
serverInfo
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
|
|
||||||
Divider()
|
Rectangle()
|
||||||
|
.fill(theme.colors.quinaryContent)
|
||||||
|
.frame(height: 1)
|
||||||
.padding(.vertical, 21)
|
.padding(.vertical, 21)
|
||||||
|
|
||||||
if viewModel.viewState.showRegistrationForm {
|
if viewModel.viewState.showRegistrationForm {
|
||||||
|
@ -63,9 +65,9 @@ struct AuthenticationRegistrationScreen: View {
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
}
|
}
|
||||||
.accentColor(theme.colors.accent)
|
|
||||||
.background(theme.colors.background.ignoresSafeArea())
|
.background(theme.colors.background.ignoresSafeArea())
|
||||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||||
|
.accentColor(theme.colors.accent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The header containing the icon, title and message.
|
/// 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 {
|
enum MockAppScreens {
|
||||||
static let appScreens: [MockScreenState.Type] = [
|
static let appScreens: [MockScreenState.Type] = [
|
||||||
MockLiveLocationSharingViewerScreenState.self,
|
MockLiveLocationSharingViewerScreenState.self,
|
||||||
|
MockAuthenticationTermsScreenState.self,
|
||||||
MockAuthenticationVerifyEmailScreenState.self,
|
MockAuthenticationVerifyEmailScreenState.self,
|
||||||
MockAuthenticationRegistrationScreenState.self,
|
MockAuthenticationRegistrationScreenState.self,
|
||||||
MockAuthenticationServerSelectionScreenState.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