mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
FTUE tweaks
- Allow login using a phone number. - Update the server when entering a full MXID during registration. - Reset the authentication service back to matrix.org after onboarding completes. - Disable zoom on ReCaptcha to fix responsiveness issue. - Tidy up unused methods.
This commit is contained in:
parent
2b6d9479a2
commit
e3bcb71b09
18 changed files with 162 additions and 54 deletions
|
@ -121,7 +121,7 @@
|
|||
|
||||
"onboarding_congratulations_title" = "Congratulations!";
|
||||
/* The placeholder string contains the user's matrix ID */
|
||||
"onboarding_congratulations_message" = "Your account %@ has been created.";
|
||||
"onboarding_congratulations_message" = "Your account %@ has been created";
|
||||
"onboarding_congratulations_personalize_button" = "Personalise profile";
|
||||
"onboarding_congratulations_home_button" = "Take me home";
|
||||
|
||||
|
@ -139,7 +139,7 @@
|
|||
"onboarding_avatar_accessibility_label" = "Profile picture";
|
||||
|
||||
"onboarding_celebration_title" = "Looking good!";
|
||||
"onboarding_celebration_message" = "Head to settings anytime to update your profile.";
|
||||
"onboarding_celebration_message" = "Head to settings anytime to update your profile";
|
||||
"onboarding_celebration_button" = "Let's go";
|
||||
|
||||
// MARK: Authentication
|
||||
|
|
|
@ -4127,7 +4127,7 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingCelebrationButton: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_button")
|
||||
}
|
||||
/// Head to settings anytime to update your profile.
|
||||
/// Head to settings anytime to update your profile
|
||||
public static var onboardingCelebrationMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_message")
|
||||
}
|
||||
|
@ -4139,7 +4139,7 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingCongratulationsHomeButton: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_congratulations_home_button")
|
||||
}
|
||||
/// Your account %@ has been created.
|
||||
/// Your account %@ has been created
|
||||
public static func onboardingCongratulationsMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "onboarding_congratulations_message", p1)
|
||||
}
|
||||
|
|
|
@ -566,6 +566,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
trackSignup()
|
||||
|
||||
completion?()
|
||||
|
||||
// Reset the authentication service back to using matrix.org
|
||||
authenticationService.reset(useDefaultServer: true)
|
||||
}
|
||||
|
||||
/// Sends a signup event to the Analytics class if onboarding has completed via the register flow.
|
||||
|
|
|
@ -174,13 +174,14 @@ class AuthenticationService: NSObject {
|
|||
}
|
||||
|
||||
/// Reset the service to a fresh state.
|
||||
func reset() {
|
||||
/// - Parameter useDefaultServer: Pass `true` to revert back to the one in `BuildSettings`, otherwise the current homeserver will be kept.
|
||||
func reset(useDefaultServer: Bool = false) {
|
||||
loginWizard = nil
|
||||
registrationWizard = nil
|
||||
softLogoutCredentials = nil
|
||||
|
||||
// The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway.
|
||||
let address = state.homeserver.addressFromUser ?? state.homeserver.address
|
||||
let address = useDefaultServer ? BuildSettings.serverConfigDefaultHomeserverUrlString : state.homeserver.addressFromUser ?? state.homeserver.address
|
||||
let identityServer = state.identityServer
|
||||
self.state = AuthenticationState(flow: .login,
|
||||
homeserverAddress: address,
|
||||
|
@ -196,27 +197,6 @@ class AuthenticationService: NSObject {
|
|||
delegate?.authenticationService(self, didReceive: token, with: transactionID) ?? false
|
||||
}
|
||||
|
||||
// /// Perform a well-known request, using the domain from the matrixId
|
||||
// func getWellKnownData(matrixId: String,
|
||||
// homeServerConnectionConfig: HomeServerConnectionConfig?) async -> WellknownResult {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /// Authenticate with a matrixId and a password
|
||||
// /// Usually call this after a successful call to getWellKnownData()
|
||||
// /// - Parameter homeServerConnectionConfig the information about the homeserver and other configuration
|
||||
// /// - Parameter matrixId the matrixId of the user
|
||||
// /// - Parameter password the password of the account
|
||||
// /// - Parameter initialDeviceName the initial device name
|
||||
// /// - Parameter deviceId the device id, optional. If not provided or null, the server will generate one.
|
||||
// func directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
// matrixId: String,
|
||||
// password: String,
|
||||
// initialDeviceName: String,
|
||||
// deviceId: String? = nil) async -> MXSession {
|
||||
//
|
||||
// }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Query the supported login flows for the supplied homeserver.
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import libPhoneNumber_iOS
|
||||
|
||||
/// Set of methods to be able to login to an existing account on a homeserver.
|
||||
///
|
||||
|
@ -42,11 +43,6 @@ class LoginWizard {
|
|||
self.state = State()
|
||||
}
|
||||
|
||||
// /// Get some information about a matrixId: displayName and avatar url
|
||||
// func profileInfo(for matrixID: String) async -> LoginProfileInfo {
|
||||
//
|
||||
// }
|
||||
|
||||
/// Login to the homeserver.
|
||||
/// - Parameters:
|
||||
/// - login: The login field. Can be a user name, or a msisdn (email or phone number) associated to the account.
|
||||
|
@ -67,6 +63,13 @@ class LoginWizard {
|
|||
password: password,
|
||||
deviceDisplayName: initialDeviceName,
|
||||
deviceID: deviceID)
|
||||
} else if let number = try? NBPhoneNumberUtil.sharedInstance().parse(login, defaultRegion: nil),
|
||||
NBPhoneNumberUtil.sharedInstance().isValidNumber(number) {
|
||||
let msisdn = login.replacingOccurrences(of: "+", with: "")
|
||||
parameters = LoginPasswordParameters(id: .thirdParty(medium: .msisdn, address: msisdn),
|
||||
password: password,
|
||||
deviceDisplayName: initialDeviceName,
|
||||
deviceID: deviceID)
|
||||
} else {
|
||||
parameters = LoginPasswordParameters(id: .user(login),
|
||||
password: password,
|
||||
|
@ -92,12 +95,6 @@ class LoginWizard {
|
|||
client: client,
|
||||
removeOtherAccounts: removeOtherAccounts)
|
||||
}
|
||||
|
||||
// /// Login to the homeserver by sending a custom JsonDict.
|
||||
// /// The data should contain at least one entry `type` with a String value.
|
||||
// func loginCustom(data: Codable) async -> MXSession {
|
||||
//
|
||||
// }
|
||||
|
||||
/// Ask the homeserver to reset the user password. The password will not be
|
||||
/// reset until `resetPasswordMailConfirmed` is successfully called.
|
||||
|
|
|
@ -195,8 +195,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
|
|||
|
||||
@MainActor private func parseUsername(_ username: String) {
|
||||
guard MXTools.isMatrixUserIdentifier(username) else { return }
|
||||
let domain = username.split(separator: ":")[1]
|
||||
let homeserverAddress = HomeserverAddress.sanitized(String(domain))
|
||||
let domain = username.components(separatedBy: ":")[1]
|
||||
let homeserverAddress = HomeserverAddress.sanitized(domain)
|
||||
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
|
|
|
@ -96,6 +96,7 @@ class AuthenticationLoginViewModelTests: XCTestCase {
|
|||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be valid to submit.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
|
@ -103,12 +104,14 @@ class AuthenticationLoginViewModelTests: XCTestCase {
|
|||
|
||||
// Then the view state should reflect that the homeserver is loading.
|
||||
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked from submission.")
|
||||
|
||||
// When updating the view model after loading a homeserver.
|
||||
viewModel.update(isLoading: false)
|
||||
|
||||
// Then the view state should reflect that the homeserver is now loaded.
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should once again be valid to submit.")
|
||||
}
|
||||
|
||||
@MainActor func testFallbackServer() {
|
||||
|
|
|
@ -92,7 +92,7 @@ struct AuthenticationRecaptchaWebView: UIViewRepresentable {
|
|||
"""
|
||||
<html>
|
||||
<head>
|
||||
<meta name='viewport' content='initial-scale=1.0' />
|
||||
<meta name='viewport' content='initial-scale=1.0, user-scalable=no' />
|
||||
<style>@media (prefers-color-scheme: dark) { body { background-color: #15191E; } }</style>
|
||||
<script type="text/javascript">
|
||||
var verifyCallback = function(response) {
|
||||
|
|
|
@ -61,6 +61,8 @@ struct AuthenticationRegistrationViewState: BindableState {
|
|||
|
||||
/// Data about the selected homeserver.
|
||||
var homeserver: AuthenticationHomeserverViewData
|
||||
/// Whether a new homeserver is currently being loaded.
|
||||
var isLoading: Bool = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationRegistrationBindings
|
||||
/// Whether or not the username field has been edited yet.
|
||||
|
@ -113,6 +115,11 @@ struct AuthenticationRegistrationViewState: BindableState {
|
|||
var hasValidCredentials: Bool {
|
||||
!isUsernameInvalid && !isPasswordInvalid
|
||||
}
|
||||
|
||||
/// `true` if valid credentials have been entered and the homeserver is loaded.
|
||||
var canSubmit: Bool {
|
||||
hasValidCredentials && !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticationRegistrationBindings {
|
||||
|
|
|
@ -59,10 +59,20 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor func update(isLoading: Bool) {
|
||||
guard state.isLoading != isLoading else { return }
|
||||
state.isLoading = isLoading
|
||||
}
|
||||
|
||||
@MainActor func update(homeserver: AuthenticationHomeserverViewData) {
|
||||
state.homeserver = homeserver
|
||||
}
|
||||
|
||||
@MainActor func update(username: String) {
|
||||
guard username != state.bindings.username else { return }
|
||||
state.bindings.username = username
|
||||
}
|
||||
|
||||
@MainActor func confirmUsernameAvailability(_ username: String) {
|
||||
guard username == state.bindings.username else { return }
|
||||
state.usernameAvailability = .available
|
||||
|
|
|
@ -21,10 +21,18 @@ protocol AuthenticationRegistrationViewModelProtocol {
|
|||
var callback: (@MainActor (AuthenticationRegistrationViewModelResult) -> Void)? { get set }
|
||||
var context: AuthenticationRegistrationViewModelType.Context { get }
|
||||
|
||||
/// Update the view to reflect that a new homeserver is being loaded.
|
||||
/// - Parameter isLoading: Whether or not the homeserver is being loaded.
|
||||
@MainActor func update(isLoading: Bool)
|
||||
|
||||
/// Update the view with new homeserver information.
|
||||
/// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`.
|
||||
@MainActor func update(homeserver: AuthenticationHomeserverViewData)
|
||||
|
||||
/// Update the username, for example to convert a full MXID into just the local part.
|
||||
/// - Parameter username: The username to be shown instead.
|
||||
@MainActor func update(username: String)
|
||||
|
||||
/// Update the view to confirm that the chosen username is available.
|
||||
/// - Parameter username: The username that was checked.
|
||||
@MainActor func confirmUsernameAvailability(_ username: String)
|
||||
|
|
|
@ -129,18 +129,58 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Show a blocking activity indicator whilst saving.
|
||||
@MainActor private func startLoading() {
|
||||
waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameter isInteractionBlocking: Whether or not the indicator blocks user interaction.
|
||||
@MainActor private func startLoading(isInteractionBlocking: Bool = true) {
|
||||
waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: isInteractionBlocking))
|
||||
|
||||
if !isInteractionBlocking {
|
||||
authenticationRegistrationViewModel.update(isLoading: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
@MainActor private func stopLoading() {
|
||||
authenticationRegistrationViewModel.update(isLoading: false)
|
||||
waitingIndicator = nil
|
||||
}
|
||||
|
||||
/// Asks the homeserver to check the supplied username's format and availability.
|
||||
/// Updates the homeserver if a full MXID is entered, then requests whether the username is valid and available.
|
||||
@MainActor private func validateUsername(_ username: String) {
|
||||
guard MXTools.isMatrixUserIdentifier(username) else {
|
||||
// Continue with availability check for a normal username.
|
||||
confirmAvailability(of: username)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise split out the domain and username and update the homeserver first.
|
||||
let components = username.dropFirst().components(separatedBy: ":")
|
||||
let domain = components[1]
|
||||
let username = components[0]
|
||||
let homeserverAddress = HomeserverAddress.sanitized(domain)
|
||||
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
currentTask = Task { [weak self] in
|
||||
do {
|
||||
try await authenticationService.startFlow(.register, for: homeserverAddress)
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
self?.updateViewModelHomeserver()
|
||||
self?.authenticationRegistrationViewModel.update(username: username)
|
||||
self?.stopLoading()
|
||||
|
||||
self?.confirmAvailability(of: username)
|
||||
} catch {
|
||||
self?.stopLoading()
|
||||
self?.handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the homeserver to check the supplied username's format and availability.
|
||||
@MainActor private func confirmAvailability(of username: String) {
|
||||
guard let registrationWizard = registrationWizard else {
|
||||
MXLog.failure("[AuthenticationRegistrationCoordinator] The registration wizard was requested before getting the login flow.")
|
||||
return
|
||||
|
@ -245,12 +285,16 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
|
|||
@MainActor private func serverSelectionCoordinator(_ coordinator: AuthenticationServerSelectionCoordinator,
|
||||
didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) {
|
||||
if result == .updated {
|
||||
let homeserver = authenticationService.state.homeserver
|
||||
authenticationRegistrationViewModel.update(homeserver: homeserver.viewData)
|
||||
updateViewModelHomeserver()
|
||||
}
|
||||
|
||||
navigationRouter.dismissModule(animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor private func updateViewModelHomeserver() {
|
||||
let homeserver = authenticationService.state.homeserver
|
||||
authenticationRegistrationViewModel.update(homeserver: homeserver.viewData)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,6 +232,45 @@ import Combine
|
|||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.")
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
|
||||
}
|
||||
|
||||
@MainActor func testLoadingServer() {
|
||||
// Given a form with valid credentials.
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be valid to submit.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
viewModel.update(isLoading: true)
|
||||
|
||||
// Then the view state should reflect that the homeserver is loading.
|
||||
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked from submission.")
|
||||
|
||||
// When updating the view model after loading a homeserver.
|
||||
viewModel.update(isLoading: false)
|
||||
|
||||
// Then the view state should reflect that the homeserver is now loaded.
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should once again be valid to submit.")
|
||||
}
|
||||
|
||||
@MainActor func testUpdatingUsername() {
|
||||
// Given a form with valid credentials.
|
||||
let fullMXID = "@bob:example.com"
|
||||
context.username = fullMXID
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid without a password.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form not be ready to submit without a password.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
|
||||
// When updating the view model with a new username.
|
||||
let localPart = "bob"
|
||||
viewModel.update(username: localPart)
|
||||
|
||||
// Then the view state should reflect that the homeserver is loading.
|
||||
XCTAssertEqual(context.username, localPart, "The username should match the value passed to the update method.")
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationRegistrationViewState.UsernameAvailability: Equatable {
|
||||
|
|
|
@ -128,7 +128,7 @@ struct AuthenticationRegistrationScreen: View {
|
|||
Text(VectorL10n.next)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.hasValidCredentials)
|
||||
.disabled(!viewModel.viewState.canSubmit)
|
||||
.accessibilityIdentifier("nextButton")
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ struct AuthenticationRegistrationScreen: View {
|
|||
|
||||
/// Sends the `next` view action so long as valid credentials have been input.
|
||||
func submit() {
|
||||
guard viewModel.viewState.hasValidCredentials else { return }
|
||||
guard viewModel.viewState.canSubmit else { return }
|
||||
viewModel.send(viewAction: .next)
|
||||
}
|
||||
|
||||
|
|
|
@ -59,26 +59,40 @@ import XCTest
|
|||
|
||||
func testReset() async throws {
|
||||
// Given a service that has begun registration.
|
||||
try await service.startFlow(.register, for: "https://matrix.org")
|
||||
try await service.startFlow(.register, for: "https://example.com")
|
||||
_ = try await service.registrationWizard?.createAccount(username: UUID().uuidString, password: UUID().uuidString, initialDeviceDisplayName: "Test")
|
||||
XCTAssertNotNil(service.loginWizard, "The login wizard should exist after starting a registration flow.")
|
||||
XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.")
|
||||
XCTAssertNotNil(service.state.homeserver.registrationFlow, "The supported registration flow should be stored after starting a registration flow.")
|
||||
XCTAssertTrue(service.isRegistrationStarted, "The service should show as having started registration.")
|
||||
XCTAssertEqual(service.state.flow, .register, "The service should show as using a registration flow.")
|
||||
XCTAssertEqual(service.state.homeserver.address, "https://matrix-client.matrix.org", "The actual homeserver address should be discovered.")
|
||||
XCTAssertEqual(service.state.homeserver.addressFromUser, "https://matrix.org", "The address from the startFlow call should be stored.")
|
||||
XCTAssertEqual(service.state.homeserver.address, "https://matrix.example.com", "The actual homeserver address should be discovered.")
|
||||
XCTAssertEqual(service.state.homeserver.addressFromUser, "https://example.com", "The address from the startFlow call should be stored.")
|
||||
|
||||
// When resetting the service.
|
||||
service.reset()
|
||||
|
||||
// Then the wizards should no longer exist.
|
||||
// Then the wizards should no longer exist, but the chosen server should be remembered.
|
||||
XCTAssertNil(service.loginWizard, "The login wizard should be cleared after calling reset.")
|
||||
XCTAssertNil(service.registrationWizard, "The registration wizard should be cleared after calling reset.")
|
||||
XCTAssertNil(service.state.homeserver.registrationFlow, "The supported registration flow should be cleared when calling reset.")
|
||||
XCTAssertFalse(service.isRegistrationStarted, "The service should not indicate it has started registration after calling reset.")
|
||||
XCTAssertEqual(service.state.flow, .login, "The flow should have been set back to login when calling reset.")
|
||||
XCTAssertEqual(service.state.homeserver.address, "https://matrix.org", "The address should reset to the value entered by the user.")
|
||||
XCTAssertEqual(service.state.homeserver.address, "https://example.com", "The address should reset to the value entered by the user.")
|
||||
}
|
||||
|
||||
func testResetDefaultServer() async throws {
|
||||
// Given a service that has begun login on one server.
|
||||
try await service.startFlow(.login, for: "https://example.com")
|
||||
XCTAssertEqual(service.state.homeserver.address, "https://matrix.example.com", "The actual homeserver address should be discovered.")
|
||||
XCTAssertEqual(service.state.homeserver.addressFromUser, "https://example.com", "The address from the startFlow call should be stored.")
|
||||
|
||||
// When resetting the service to use the default server.
|
||||
service.reset(useDefaultServer: true)
|
||||
|
||||
// Then the service should reset back to the default server.
|
||||
XCTAssertEqual(service.state.homeserver.address, BuildSettings.serverConfigDefaultHomeserverUrlString,
|
||||
"The address should reset to the value configured in the build settings.")
|
||||
}
|
||||
|
||||
func testHomeserverState() async throws {
|
||||
|
|
1
changelog.d/6428.wip
Normal file
1
changelog.d/6428.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Check for a phone number during login and send an MSISDN when using the new flow.
|
1
changelog.d/6429.wip
Normal file
1
changelog.d/6429.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Fix ReCaptcha form sometimes being slow to react to taps in the new flow.
|
1
changelog.d/6430.wip
Normal file
1
changelog.d/6430.wip
Normal file
|
@ -0,0 +1 @@
|
|||
When entering a full MXID during registration on the new flow, update the homeserver to match.
|
Loading…
Reference in a new issue