Add registration terms screen. (#6128)

Begin implementing Auth Terms coordinator.
This commit is contained in:
Doug 2022-05-10 22:34:35 +01:00 committed by GitHub
parent 4a9ace0cf3
commit 1b160b5f25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 722 additions and 4 deletions

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "authentication_terms_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -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

View file

@ -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.";

View file

@ -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")

View file

@ -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")

View file

@ -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)

View file

@ -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.

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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))
)
}
}

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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)) { }
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
View file

@ -0,0 +1 @@
Authentication: Create terms screen.