mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add a WebRegistrationScreen (not included in the flow yet). (#3281)
This commit is contained in:
@@ -13,6 +13,8 @@ struct LoginHomeserver: Equatable {
|
||||
let address: String
|
||||
/// The types login supported by the homeserver.
|
||||
var loginMode: LoginMode
|
||||
/// A temporary helper URL that can be used for registration.
|
||||
var registrationHelperURL: URL?
|
||||
|
||||
/// Creates a new homeserver value.
|
||||
init(address: String, loginMode: LoginMode) {
|
||||
|
||||
@@ -23,6 +23,7 @@ enum LoginScreenCoordinatorAction {
|
||||
case signedIn(UserSessionProtocol)
|
||||
}
|
||||
|
||||
// Note: This code was brought over from Riot, we should move the authentication service logic into the view model.
|
||||
final class LoginScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: LoginScreenCoordinatorParameters
|
||||
private var viewModel: LoginScreenViewModelProtocol
|
||||
|
||||
+2
-2
@@ -66,9 +66,9 @@ struct ServerConfirmationScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
|
||||
static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(),
|
||||
authenticationFlow: .login)
|
||||
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationServiceProxy(),
|
||||
static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(),
|
||||
authenticationFlow: .register)
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ enum ServerSelectionScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
// Note: This code was brought over from Riot, we should move the authentication service logic into the view model.
|
||||
final class ServerSelectionScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ServerSelectionScreenCoordinatorParameters
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ enum SoftLogoutScreenCoordinatorResult: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This code was brought over from Riot, we should move the authentication service logic into the view model.
|
||||
final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SoftLogoutScreenCoordinatorParameters
|
||||
private var viewModel: SoftLogoutScreenViewModelProtocol
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct WebRegistrationScreen: View {
|
||||
@ObservedObject var context: WebRegistrationScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
WebRegistrationWebView(url: context.viewState.url, viewModelContext: context)
|
||||
.navigationTitle(L10n.screenCreateAccountTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(.visible, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationWebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
let viewModelContext: WebRegistrationScreenViewModel.Context
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
context.coordinator.webView
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) { }
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(url: url, viewModelContext: viewModelContext)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, WKUIDelegate {
|
||||
private let url: URL
|
||||
private let viewModelContext: WebRegistrationScreenViewModel.Context
|
||||
|
||||
private(set) var webView: WKWebView!
|
||||
|
||||
init(url: URL, viewModelContext: WebRegistrationScreenViewModel.Context) {
|
||||
self.url = url
|
||||
self.viewModelContext = viewModelContext
|
||||
|
||||
super.init()
|
||||
|
||||
let eventHandlerName = "elementx"
|
||||
let userContentController = WKUserContentController()
|
||||
userContentController.add(WKScriptMessageHandlerWrapper(self), name: eventHandlerName)
|
||||
|
||||
let eventHandlerScript = """
|
||||
window.addEventListener(
|
||||
"mobileregistrationresponse",
|
||||
(event) => {
|
||||
window.webkit.messageHandlers.\(eventHandlerName).postMessage(JSON.stringify(event.detail));
|
||||
},
|
||||
false,
|
||||
);
|
||||
"""
|
||||
|
||||
let userScript = WKUserScript(source: eventHandlerScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
|
||||
userContentController.addUserScript(userScript)
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.userContentController = userContentController
|
||||
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.uiDelegate = self
|
||||
webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else {
|
||||
MXLog.error("Unexpected response.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = try? JSONDecoder().decode(WebRegistrationCredentials.self, from: jsonData) else {
|
||||
MXLog.error("Invalid response.")
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.info("Received login credentials.")
|
||||
Task { await viewModelContext.send(viewAction: .signedIn(credentials)) }
|
||||
}
|
||||
|
||||
// MARK: WKUIDelegate
|
||||
|
||||
func webView(_ webView: WKWebView,
|
||||
createWebViewWith configuration: WKWebViewConfiguration,
|
||||
for navigationAction: WKNavigationAction,
|
||||
windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
if let url = navigationAction.request.url, UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Avoids retain loops between the configuration and webView coordinator
|
||||
private class WKScriptMessageHandlerWrapper: NSObject, WKScriptMessageHandler {
|
||||
private weak var coordinator: Coordinator?
|
||||
|
||||
init(_ coordinator: Coordinator) {
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
// MARK: WKScriptMessageHandler
|
||||
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
coordinator?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct WebRegistrationScreen_Previews: PreviewProvider {
|
||||
static let viewModel = WebRegistrationScreenViewModel(registrationHelperURL: "https://develop.element.io/#/mobile_register")
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
WebRegistrationScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct WebRegistrationScreenCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProtocol
|
||||
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum WebRegistrationScreenCoordinatorAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(UserSessionProtocol)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This code was based on the LoginScreen, we should move the authentication service logic into the view model.
|
||||
final class WebRegistrationScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: WebRegistrationScreenCoordinatorParameters
|
||||
private let viewModel: WebRegistrationScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<WebRegistrationScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: WebRegistrationScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
guard let registrationHelperURL = parameters.authenticationService.homeserver.value.registrationHelperURL else {
|
||||
MXLog.error("Attempted registration without a helper URL.")
|
||||
fatalError("A helper URL is required.")
|
||||
}
|
||||
viewModel = WebRegistrationScreenViewModel(registrationHelperURL: registrationHelperURL)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .signedIn(let credentials):
|
||||
Task { await self.completeRegistration(using: credentials) }
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(WebRegistrationScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func completeRegistration(using credentials: WebRegistrationCredentials) async {
|
||||
switch await parameters.authenticationService.completeWebRegistration(using: credentials) {
|
||||
case .success(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed registration: \(error)")
|
||||
parameters.userIndicatorController.alertInfo = .init(id: UUID(), title: L10n.errorUnknown, message: String(describing: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WebRegistrationScreenViewModelAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(WebRegistrationCredentials)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationScreenViewState: BindableState {
|
||||
var url: URL
|
||||
var bindings = WebRegistrationScreenViewStateBindings()
|
||||
}
|
||||
|
||||
struct WebRegistrationScreenViewStateBindings { }
|
||||
|
||||
enum WebRegistrationScreenViewAction: CustomStringConvertible {
|
||||
case cancel
|
||||
case signedIn(WebRegistrationCredentials)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cancel: "cancel"
|
||||
case .signedIn: "signedIn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRegistrationCredentials: Decodable {
|
||||
let userID: String
|
||||
let accessToken: String
|
||||
let deviceID: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userID = "user_id"
|
||||
case accessToken = "access_token"
|
||||
case deviceID = "device_id"
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias WebRegistrationScreenViewModelType = StateStoreViewModel<WebRegistrationScreenViewState, WebRegistrationScreenViewAction>
|
||||
|
||||
class WebRegistrationScreenViewModel: WebRegistrationScreenViewModelType, WebRegistrationScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<WebRegistrationScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(registrationHelperURL: URL) {
|
||||
super.init(initialViewState: WebRegistrationScreenViewState(url: registrationHelperURL))
|
||||
}
|
||||
|
||||
override func process(viewAction: WebRegistrationScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
actionsSubject.send(.cancel)
|
||||
case .signedIn(let credentials):
|
||||
actionsSubject.send(.signedIn(credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2022-2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol WebRegistrationScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<WebRegistrationScreenViewModelAction, Never> { get }
|
||||
var context: WebRegistrationScreenViewModelType.Context { get }
|
||||
}
|
||||
Reference in New Issue
Block a user