mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 15:09:31 +02:00
Add ReCaptcha screen (#6135)
Support dark mode in MXKAuthenticationRecaptchaWebView. Begin implementing the ReCaptcha coordinator.
This commit is contained in:
@@ -52,6 +52,8 @@
|
||||
"authentication_terms_title" = "Privacy policy";
|
||||
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
|
||||
|
||||
"authentication_recaptcha_message" = "This server would like to make sure you are not a robot";
|
||||
|
||||
// MARK: Spaces WIP
|
||||
"spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer.";
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ import Foundation
|
||||
|
||||
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
|
||||
public extension VectorL10n {
|
||||
/// This server would like to make sure you are not a robot
|
||||
static var authenticationRecaptchaMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_recaptcha_message")
|
||||
}
|
||||
/// Join millions for free on the largest public server
|
||||
static var authenticationRegistrationMatrixDescription: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_matrix_description")
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
[self.ssoButton setTitle:[VectorL10n authLoginSingleSignOn] forState:UIControlStateNormal];
|
||||
[self.ssoButton setTitle:[VectorL10n authLoginSingleSignOn] forState:UIControlStateHighlighted];
|
||||
self.ssoButton.backgroundColor = ThemeService.shared.theme.tintColor;
|
||||
|
||||
self.recaptchaContainer.backgroundColor = ThemeService.shared.theme.backgroundColor;
|
||||
|
||||
if (self.userLoginTextField.placeholder)
|
||||
{
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
*/
|
||||
|
||||
#import "MXKAuthenticationRecaptchaWebView.h"
|
||||
#import "ThemeService.h"
|
||||
|
||||
NSString *kMXKRecaptchaHTMLString = @"<html> \
|
||||
<head> \
|
||||
<meta name='viewport' content='initial-scale=1.0' /> \
|
||||
<style>@media (prefers-color-scheme: dark) { body { background-color: #15191E; } }</style> \
|
||||
<script type=\"text/javascript\"> \
|
||||
var verifyCallback = function(response) { \
|
||||
/* Generic method to make a bridge between JS and the WKWebView*/ \
|
||||
@@ -33,7 +35,8 @@ var verifyCallback = function(response) { \
|
||||
var onloadCallback = function() { \
|
||||
grecaptcha.render('recaptcha_widget', { \
|
||||
'sitekey' : '%@', \
|
||||
'callback': verifyCallback \
|
||||
'callback': verifyCallback, \
|
||||
'theme': '%@' \
|
||||
}); \
|
||||
}; \
|
||||
</script> \
|
||||
@@ -78,7 +81,9 @@ var onloadCallback = function() { \
|
||||
[self addSubview:activityIndicator];
|
||||
[activityIndicator startAnimating];
|
||||
|
||||
NSString *htmlString = [NSString stringWithFormat:kMXKRecaptchaHTMLString, siteKey];
|
||||
NSString *theme = ThemeService.shared.isCurrentThemeDark ? @"dark" : @"light";
|
||||
|
||||
NSString *htmlString = [NSString stringWithFormat:kMXKRecaptchaHTMLString, siteKey, theme];
|
||||
|
||||
[self loadHTMLString:htmlString baseURL:[NSURL URLWithString:homeServer]];
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ targets:
|
||||
- path: ../Riot/Managers/Widgets/WidgetConstants.m
|
||||
- path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift
|
||||
- path: ../Riot/Modules/MatrixKit
|
||||
excludes:
|
||||
- "**/MXKAuthenticationRecaptchaWebView.*"
|
||||
- path: ../Riot/Modules/Analytics
|
||||
- path: ../Riot/Managers/UserSessions
|
||||
- path: ../Riot/Managers/AppInfo/
|
||||
|
||||
@@ -70,6 +70,8 @@ targets:
|
||||
- path: ../Riot/Assets/SharedImages.xcassets
|
||||
buildPhase: resources
|
||||
- path: ../Riot/Modules/MatrixKit
|
||||
excludes:
|
||||
- "**/MXKAuthenticationRecaptchaWebView.*"
|
||||
- path: ../Riot/Modules/Analytics
|
||||
- path: ../Riot/Managers/UserSessions
|
||||
excludes:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// 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: View model
|
||||
|
||||
enum AuthenticationReCaptchaViewModelResult {
|
||||
/// Perform the ReCaptcha stage with the associated response.
|
||||
case validate(String)
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationReCaptchaViewState: BindableState {
|
||||
/// The `sitekey` passed to the ReCaptcha widget.
|
||||
let siteKey: String
|
||||
/// The homeserver URL used for the web view.
|
||||
let homeserverURL: URL
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings = AuthenticationReCaptchaBindings()
|
||||
}
|
||||
|
||||
struct AuthenticationReCaptchaBindings {
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<Int>?
|
||||
}
|
||||
|
||||
enum AuthenticationReCaptchaViewAction {
|
||||
/// Perform the ReCaptcha stage with the associated response.
|
||||
case validate(String)
|
||||
/// Cancel the flow.
|
||||
case cancel
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
typealias AuthenticationReCaptchaViewModelType = StateStoreViewModel<AuthenticationReCaptchaViewState,
|
||||
Never,
|
||||
AuthenticationReCaptchaViewAction>
|
||||
|
||||
class AuthenticationReCaptchaViewModel: AuthenticationReCaptchaViewModelType, AuthenticationReCaptchaViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@MainActor var callback: ((AuthenticationReCaptchaViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(siteKey: String, homeserverURL: URL) {
|
||||
super.init(initialViewState: AuthenticationReCaptchaViewState(siteKey: siteKey,
|
||||
homeserverURL: homeserverURL))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: AuthenticationReCaptchaViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
Task { await callback?(.cancel) }
|
||||
case .validate(let response):
|
||||
Task { await callback?(.validate(response)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Foundation
|
||||
|
||||
protocol AuthenticationReCaptchaViewModelProtocol {
|
||||
|
||||
@MainActor var callback: ((AuthenticationReCaptchaViewModelResult) -> Void)? { get set }
|
||||
var context: AuthenticationReCaptchaViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// 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 AuthenticationReCaptchaCoordinatorParameters {
|
||||
let authenticationService: AuthenticationService
|
||||
let registrationWizard: RegistrationWizard
|
||||
/// The ReCaptcha widget's site key.
|
||||
let siteKey: String
|
||||
}
|
||||
|
||||
enum AuthenticationReCaptchaCoordinatorResult {
|
||||
/// The screen completed with the associated registration result.
|
||||
case completed(RegistrationResult)
|
||||
/// The user would like to cancel the registration.
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class AuthenticationReCaptchaCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: AuthenticationReCaptchaCoordinatorParameters
|
||||
private let authenticationReCaptchaHostingController: UIViewController
|
||||
private var authenticationReCaptchaViewModel: AuthenticationReCaptchaViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
/// The wizard used to handle the registration flow.
|
||||
private 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: ((AuthenticationReCaptchaCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@MainActor init(parameters: AuthenticationReCaptchaCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
guard let homeserverURL = URL(string: parameters.authenticationService.state.homeserver.address) else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let viewModel = AuthenticationReCaptchaViewModel(siteKey: parameters.siteKey, homeserverURL: homeserverURL)
|
||||
let view = AuthenticationReCaptchaScreen(viewModel: viewModel.context)
|
||||
authenticationReCaptchaViewModel = viewModel
|
||||
authenticationReCaptchaHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationReCaptchaHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[AuthenticationReCaptchaCoordinator] did start.")
|
||||
Task { await setupViewModel() }
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.authenticationReCaptchaHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`.
|
||||
@MainActor private func setupViewModel() {
|
||||
authenticationReCaptchaViewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[AuthenticationReCaptchaCoordinator] AuthenticationReCaptchaViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case .validate(let response):
|
||||
self.performReCaptcha(response)
|
||||
case .cancel:
|
||||
#warning("Reset the flow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Performs the ReCaptcha stage with the supplied response string.
|
||||
@MainActor private func performReCaptcha(_ response: String) {
|
||||
startLoading()
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
let result = try await registrationWizard.performReCaptcha(response: response)
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
callback?(.completed(result))
|
||||
self?.stopLoading()
|
||||
} catch {
|
||||
self?.stopLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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 MockAuthenticationReCaptchaScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case standard
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
AuthenticationReCaptchaScreen.self
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: AuthenticationReCaptchaViewModel
|
||||
switch self {
|
||||
case .standard:
|
||||
viewModel = AuthenticationReCaptchaViewModel(siteKey: "12345", homeserverURL: URL(string: "https://matrix-client.matrix.org")!)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel], AnyView(AuthenticationReCaptchaScreen(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// 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 AuthenticationReCaptchaUITests: MockScreenTest {
|
||||
// Nothing to test as the view only has a single state.
|
||||
}
|
||||
@@ -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 AuthenticationReCaptchaViewModelTests: XCTestCase {
|
||||
// Nothing to test as the view model has no mutable state.
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// 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 AuthenticationReCaptchaScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
@State private var isLoading = false
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: AuthenticationReCaptchaViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
VStack(spacing: 40) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.horizontal, 16)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
recaptcha
|
||||
.frame(minHeight: 500)
|
||||
}
|
||||
.readableFrame()
|
||||
.frame(minHeight: geometry.size.height)
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.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.onboardingCongratulationsIcon)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(VectorL10n.authenticationRegistrationTitle)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationRecaptchaMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
/// The web view that shows the ReCaptcha to the user.
|
||||
var recaptcha: some View {
|
||||
AuthenticationRecaptchaWebView(siteKey: viewModel.viewState.siteKey,
|
||||
homeserverURL: viewModel.viewState.homeserverURL,
|
||||
isLoading: $isLoading) { response in
|
||||
viewModel.send(viewAction: .validate(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AuthenticationReCaptcha_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockAuthenticationReCaptchaScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
stateRenderer.screenGroup(addNavigation: true)
|
||||
.navigationViewStyle(.stack)
|
||||
.theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// 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
|
||||
import WebKit
|
||||
|
||||
struct AuthenticationRecaptchaWebView: UIViewRepresentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Public
|
||||
|
||||
/// The `siteKey` string to pass to the ReCaptcha widget.
|
||||
let siteKey: String
|
||||
/// The homeserver's URL, used so ReCaptcha can validate where the request is coming from.
|
||||
let homeserverURL: URL
|
||||
|
||||
/// A binding to boolean that controls whether or not a loading spinner should be shown.
|
||||
@Binding var isLoading: Bool
|
||||
|
||||
/// The completion called when the ReCaptcha was successful. The response string
|
||||
/// is passed into the closure as the only argument.
|
||||
let completion: (String) -> Void
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WKWebView()
|
||||
webView.navigationDelegate = context.coordinator
|
||||
|
||||
#if DEBUG
|
||||
// Use a randomised user agent to encourage the ReCaptcha to show a challenge.
|
||||
webView.customUserAgent = "Show Me The Traffic Lights \(Float.random(in: 1...100))"
|
||||
#endif
|
||||
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
let recaptchaTheme: Coordinator.ReCaptchaTheme = theme.isDark ? .dark : .light
|
||||
webView.loadHTMLString(context.coordinator.htmlString(with: siteKey, using: recaptchaTheme), baseURL: homeserverURL)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
let coordinator = Coordinator(isLoading: $isLoading)
|
||||
coordinator.completion = completion
|
||||
return coordinator
|
||||
}
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
/// The theme used to render the ReCaptcha
|
||||
enum ReCaptchaTheme: String { case light, dark }
|
||||
|
||||
/// A binding to boolean that controls whether or not a loading spinner should be shown.
|
||||
@Binding var isLoading: Bool
|
||||
|
||||
/// The completion called when the ReCaptcha was successful. The response string
|
||||
/// is passed into the closure as the only argument.
|
||||
var completion: ((String) -> Void)?
|
||||
|
||||
init(isLoading: Binding<Bool>) {
|
||||
self._isLoading = isLoading
|
||||
}
|
||||
|
||||
/// Generates the HTML page to show for the given `siteKey` and `theme`.
|
||||
func htmlString(with siteKey: String, using theme: ReCaptchaTheme) -> String {
|
||||
"""
|
||||
<html>
|
||||
<head>
|
||||
<meta name='viewport' content='initial-scale=1.0' />
|
||||
<style>@media (prefers-color-scheme: dark) { body { background-color: #15191E; } }</style>
|
||||
<script type="text/javascript">
|
||||
var verifyCallback = function(response) {
|
||||
/* Generic method to make a bridge between JS and the WKWebView*/
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('src', 'js:' + JSON.stringify({'action': 'verifyCallback', 'response': response}));
|
||||
|
||||
document.documentElement.appendChild(iframe);
|
||||
iframe.parentNode.removeChild(iframe);
|
||||
iframe = null;
|
||||
};
|
||||
var onloadCallback = function() {
|
||||
grecaptcha.render('recaptcha_widget', {
|
||||
'sitekey' : '\(siteKey)',
|
||||
'callback': verifyCallback,
|
||||
'theme': '\(theme.rawValue)'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body style="margin: 16px;">
|
||||
<div id="recaptcha_widget"></div>
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
|
||||
guard
|
||||
let url = navigationAction.request.url,
|
||||
// Listen only to scheme of the JS-WKWebView bridge
|
||||
navigationAction.request.url?.scheme == "js"
|
||||
else { return .allow }
|
||||
|
||||
guard
|
||||
let jsonString = url.path.removingPercentEncoding,
|
||||
let jsonData = jsonString.data(using: .utf8),
|
||||
let parameters = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String: String],
|
||||
parameters["action"] == "verifyCallback",
|
||||
let response = parameters["response"]
|
||||
else { return .cancel }
|
||||
|
||||
completion?(response)
|
||||
|
||||
return .cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ struct AuthenticationTermsScreen: View {
|
||||
button
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: OnboardingMetrics.maxContentWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
.readableFrame()
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
|
||||
@@ -37,9 +37,9 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
/// The authentication service used for the registration.
|
||||
var authenticationService: AuthenticationService { parameters.authenticationService }
|
||||
private var authenticationService: AuthenticationService { parameters.authenticationService }
|
||||
/// The wizard used to handle the registration flow.
|
||||
var registrationWizard: RegistrationWizard { parameters.registrationWizard }
|
||||
private var registrationWizard: RegistrationWizard { parameters.registrationWizard }
|
||||
|
||||
private var currentTask: Task<Void, Error>? {
|
||||
willSet {
|
||||
|
||||
@@ -21,6 +21,7 @@ import Foundation
|
||||
enum MockAppScreens {
|
||||
static let appScreens: [MockScreenState.Type] = [
|
||||
MockLiveLocationSharingViewerScreenState.self,
|
||||
MockAuthenticationReCaptchaScreenState.self,
|
||||
MockAuthenticationTermsScreenState.self,
|
||||
MockAuthenticationVerifyEmailScreenState.self,
|
||||
MockAuthenticationRegistrationScreenState.self,
|
||||
|
||||
@@ -51,6 +51,8 @@ targets:
|
||||
- path: ../Riot/Managers/Locale/LocaleProviderType.swift
|
||||
- path: ../Riot/Managers/Locale/LocaleProvider.swift
|
||||
- path: ../Riot/Modules/MatrixKit
|
||||
excludes:
|
||||
- "**/MXKAuthenticationRecaptchaWebView.*"
|
||||
- path: ../Riot/Modules/Analytics
|
||||
- path: ../Riot/Managers/UserSessions
|
||||
- path: ../Riot/Managers/AppInfo/
|
||||
|
||||
@@ -1 +1 @@
|
||||
Authentication: Create terms screen.
|
||||
Authentication: Create terms and ReCaptcha screens.
|
||||
|
||||
Reference in New Issue
Block a user