mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-18 07:28:28 +02:00
Replace ReCaptcha navigation delegate with a WKUserContentController. Move callback property closures onto the MainActor. Show a loading indicator whilst waiting for the authentication service to start. Move nextUncompletedStage into FlowResult. Handle text field actions during authentication. Remove scroll view tweaks in server selection screen following EMS banner removal.
291 lines
14 KiB
Swift
291 lines
14 KiB
Swift
//
|
|
// 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 Foundation
|
|
|
|
/// Set of methods to be able to create an account on a homeserver.
|
|
///
|
|
/// Common scenario to register an account successfully:
|
|
/// - Call `registrationFlow` to check that you application supports all the mandatory registration stages
|
|
/// - Call `createAccount` to start the account creation
|
|
/// - Fulfil all mandatory stages using the methods `performReCaptcha` `acceptTerms` `dummy`, etc.
|
|
///
|
|
/// More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md
|
|
/// and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
|
|
class RegistrationWizard {
|
|
struct State {
|
|
var currentSession: String?
|
|
var isRegistrationStarted = false
|
|
var currentThreePIDData: ThreePIDData?
|
|
|
|
var clientSecret = UUID().uuidString
|
|
var sendAttempt: UInt = 0
|
|
}
|
|
|
|
let client: MXRestClient
|
|
let sessionCreator: SessionCreator
|
|
|
|
private(set) var state: State
|
|
|
|
/// This is the current ThreePID, waiting for validation. The SDK will store it in database, so it can be
|
|
/// restored even if the app has been killed during the registration
|
|
var currentThreePID: String? {
|
|
guard let threePid = state.currentThreePIDData?.threePID else { return nil }
|
|
|
|
switch threePid {
|
|
case .email(let string):
|
|
return string
|
|
case .msisdn(let msisdn, _):
|
|
return state.currentThreePIDData?.registrationResponse.formattedMSISDN ?? msisdn
|
|
}
|
|
}
|
|
|
|
/// True when login and password have been sent with success to the homeserver,
|
|
/// i.e. `createAccount` has been called successfully.
|
|
var isRegistrationStarted: Bool {
|
|
state.isRegistrationStarted
|
|
}
|
|
|
|
init(client: MXRestClient, sessionCreator: SessionCreator = SessionCreator()) {
|
|
self.client = client
|
|
self.sessionCreator = sessionCreator
|
|
|
|
self.state = State()
|
|
}
|
|
|
|
/// Call this method to get the possible registration flow of the current homeserver.
|
|
/// It can be useful to ensure that your application implementation supports all the stages
|
|
/// required to create an account. If it is not the case, you will have to use the web fallback
|
|
/// to let the user create an account with your application.
|
|
/// See `AuthenticationService.getFallbackUrl`
|
|
func registrationFlow() async throws -> RegistrationResult {
|
|
let parameters = RegistrationParameters()
|
|
|
|
do {
|
|
let result = try await performRegistrationRequest(parameters: parameters)
|
|
return result
|
|
} catch {
|
|
// Map M_FORBIDDEN into a registration error.
|
|
guard let mxError = MXError(nsError: error), mxError.errcode == kMXErrCodeStringForbidden else { throw error }
|
|
MXLog.warning("[RegistrationWizard] Registration is disabled for the selected server.")
|
|
throw RegistrationError.registrationDisabled
|
|
}
|
|
}
|
|
|
|
/// Can be call to check is the desired username is available for registration on the current homeserver.
|
|
/// It may also fails if the desired username is not correctly formatted or does not follow any restriction on
|
|
/// the homeserver. Ex: username with only digits may be rejected.
|
|
/// - Parameter username the desired username. Ex: "alice"
|
|
func registrationAvailable(username: String) async throws -> Bool {
|
|
try await client.isUsernameAvailable(username)
|
|
}
|
|
|
|
/// This is the first method to call in order to create an account and start the registration process.
|
|
///
|
|
/// - Parameter username the desired username. Ex: "alice"
|
|
/// - Parameter password the desired password
|
|
/// - Parameter initialDeviceDisplayName the device display name
|
|
func createAccount(username: String?,
|
|
password: String?,
|
|
initialDeviceDisplayName: String?) async throws -> RegistrationResult {
|
|
let parameters = RegistrationParameters(username: username, password: password, initialDeviceDisplayName: initialDeviceDisplayName)
|
|
let result = try await performRegistrationRequest(parameters: parameters, isCreatingAccount: true)
|
|
state.isRegistrationStarted = true
|
|
return result
|
|
}
|
|
|
|
/// Perform the "m.login.recaptcha" stage.
|
|
///
|
|
/// - Parameter response: The response from ReCaptcha
|
|
func performReCaptcha(response: String) async throws -> RegistrationResult {
|
|
guard let session = state.currentSession else {
|
|
MXLog.error("[RegistrationWizard] performReCaptcha: Missing authentication session, createAccount hasn't been called.")
|
|
throw RegistrationError.createAccountNotCalled
|
|
}
|
|
|
|
let parameters = RegistrationParameters(auth: AuthenticationParameters.captchaParameters(session: session, captchaResponse: response))
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
}
|
|
|
|
/// Perform the "m.login.terms" stage.
|
|
func acceptTerms() async throws -> RegistrationResult {
|
|
guard let session = state.currentSession else {
|
|
MXLog.error("[RegistrationWizard] acceptTerms: Missing authentication session, createAccount hasn't been called.")
|
|
throw RegistrationError.createAccountNotCalled
|
|
}
|
|
|
|
let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeTerms, session: session))
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
}
|
|
|
|
/// Perform the "m.login.dummy" stage.
|
|
func dummy() async throws -> RegistrationResult {
|
|
guard let session = state.currentSession else {
|
|
MXLog.error("[RegistrationWizard] dummy: Missing authentication session, createAccount hasn't been called.")
|
|
throw RegistrationError.createAccountNotCalled
|
|
}
|
|
|
|
let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeDummy, session: session))
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
}
|
|
|
|
/// Perform the "m.login.email.identity" or "m.login.msisdn" stage.
|
|
///
|
|
/// - Parameter threePID: the threePID to add to the account. If this is an email, the homeserver will send an email
|
|
/// to validate it. For a msisdn a SMS will be sent.
|
|
func addThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult {
|
|
state.currentThreePIDData = nil
|
|
return try await sendThreePID(threePID: threePID)
|
|
}
|
|
|
|
/// Ask the homeserver to send again the current threePID (email or msisdn).
|
|
func sendAgainThreePID() async throws -> RegistrationResult {
|
|
guard let threePID = state.currentThreePIDData?.threePID else {
|
|
MXLog.error("[RegistrationWizard] sendAgainThreePID: Missing authentication session, createAccount hasn't been called.")
|
|
throw RegistrationError.createAccountNotCalled
|
|
}
|
|
return try await sendThreePID(threePID: threePID)
|
|
}
|
|
|
|
/// Send the code received by SMS to validate a msisdn.
|
|
/// If the code is correct, the registration request will be executed to validate the msisdn.
|
|
func handleValidateThreePID(code: String) async throws -> RegistrationResult {
|
|
return try await validateThreePid(code: code)
|
|
}
|
|
|
|
/// Useful to poll the homeserver when waiting for the email to be validated by the user.
|
|
/// Once the email is validated, this method will return successfully.
|
|
func checkIfEmailHasBeenValidated() async throws -> RegistrationResult {
|
|
guard let parameters = state.currentThreePIDData?.registrationParameters else {
|
|
MXLog.error("[RegistrationWizard] checkIfEmailHasBeenValidated: The current 3pid data hasn't been stored in the state.")
|
|
throw RegistrationError.missingThreePIDData
|
|
}
|
|
|
|
do {
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
} catch {
|
|
// An unauthorized error indicates that the user hasn't tapped the link yet.
|
|
guard isUnauthorized(error) else { throw error }
|
|
throw RegistrationError.waitingForThreePIDValidation
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func validateThreePid(code: String) async throws -> RegistrationResult {
|
|
guard let threePIDData = state.currentThreePIDData else {
|
|
MXLog.error("[RegistrationWizard] validateThreePid: There is no third party ID data stored in the state.")
|
|
throw RegistrationError.missingThreePIDData
|
|
}
|
|
|
|
guard let submitURL = threePIDData.registrationResponse.submitURL else {
|
|
MXLog.error("[RegistrationWizard] validateThreePid: The third party ID data doesn't contain a submitURL.")
|
|
throw RegistrationError.missingThreePIDURL
|
|
}
|
|
|
|
|
|
let validationBody = ThreePIDValidationCodeBody(clientSecret: state.clientSecret,
|
|
sessionID: threePIDData.registrationResponse.sessionID,
|
|
code: code)
|
|
|
|
#warning("Seems odd to pass a nil baseURL and then the url as the path, yet this is how MXK3PID works")
|
|
guard let httpClient = MXHTTPClient(baseURL: nil, andOnUnrecognizedCertificateBlock: nil) else {
|
|
MXLog.error("[RegistrationWizard] validateThreePid: Failed to create an MXHTTPClient.")
|
|
throw RegistrationError.threePIDClientFailure
|
|
}
|
|
guard try await httpClient.validateThreePIDCode(submitURL: submitURL, validationBody: validationBody) else {
|
|
MXLog.error("[RegistrationWizard] validateThreePid: Third party ID validation failed.")
|
|
throw RegistrationError.threePIDValidationFailure
|
|
}
|
|
|
|
let parameters = threePIDData.registrationParameters
|
|
MXLog.failure("This method used to add a 3-second delay to the request. This should be moved to the caller of `handleValidateThreePID`.")
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
}
|
|
|
|
private func sendThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult {
|
|
guard let session = state.currentSession else {
|
|
MXLog.error("[RegistrationWizard] sendThreePID: Missing authentication session, createAccount hasn't been called.")
|
|
throw RegistrationError.createAccountNotCalled
|
|
}
|
|
|
|
let response = try await client.requestTokenDuringRegistration(for: threePID,
|
|
clientSecret: state.clientSecret,
|
|
sendAttempt: state.sendAttempt)
|
|
|
|
state.sendAttempt += 1
|
|
|
|
let threePIDCredentials = ThreePIDCredentials(clientSecret: state.clientSecret, sessionID: response.sessionID)
|
|
let authenticationParameters: AuthenticationParameters
|
|
switch threePID {
|
|
case .email:
|
|
authenticationParameters = AuthenticationParameters.emailIdentityParameters(session: session, threePIDCredentials: threePIDCredentials)
|
|
case .msisdn:
|
|
authenticationParameters = AuthenticationParameters.msisdnIdentityParameters(session: session, threePIDCredentials: threePIDCredentials)
|
|
}
|
|
|
|
let parameters = RegistrationParameters(auth: authenticationParameters)
|
|
|
|
state.currentThreePIDData = ThreePIDData(threePID: threePID, registrationResponse: response, registrationParameters: parameters)
|
|
|
|
do {
|
|
// Send the session id for the first time
|
|
return try await performRegistrationRequest(parameters: parameters)
|
|
} catch {
|
|
// An unauthorized error means that it was accepted and is awaiting validation.
|
|
guard isUnauthorized(error) else { throw error }
|
|
throw RegistrationError.waitingForThreePIDValidation
|
|
}
|
|
}
|
|
|
|
private func performRegistrationRequest(parameters: RegistrationParameters, isCreatingAccount: Bool = false) async throws -> RegistrationResult {
|
|
do {
|
|
let response = try await client.register(parameters: parameters)
|
|
let credentials = MXCredentials(loginResponse: response, andDefaultCredentials: client.credentials)
|
|
return .success(sessionCreator.createSession(credentials: credentials, client: client))
|
|
} catch {
|
|
let nsError = error as NSError
|
|
|
|
guard
|
|
let jsonResponse = nsError.userInfo[MXHTTPClientErrorResponseDataKey] as? [String: Any],
|
|
let authenticationSession = MXAuthenticationSession(fromJSON: jsonResponse)
|
|
else { throw error }
|
|
|
|
state.currentSession = authenticationSession.session
|
|
let flowResult = authenticationSession.flowResult
|
|
|
|
if isCreatingAccount || isRegistrationStarted {
|
|
return try await handleMandatoryDummyStage(flowResult: flowResult)
|
|
}
|
|
|
|
return .flowResponse(flowResult)
|
|
}
|
|
}
|
|
|
|
/// Checks for a mandatory dummy stage and handles it automatically when possible.
|
|
private func handleMandatoryDummyStage(flowResult: FlowResult) async throws -> RegistrationResult {
|
|
// If the dummy stage is mandatory, do the dummy stage now
|
|
guard flowResult.missingStages.contains(where: { $0.isDummy && $0.isMandatory }) else { return .flowResponse(flowResult) }
|
|
return try await dummy()
|
|
}
|
|
|
|
/// Checks whether an error is an `M_UNAUTHORIZED` for handling third party ID responses.
|
|
private func isUnauthorized(_ error: Error) -> Bool {
|
|
guard let mxError = MXError(nsError: error) else { return false }
|
|
return mxError.errcode == kMXErrCodeStringUnauthorized
|
|
}
|
|
}
|