mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Update use case, registration and login screens.
Remove "Custom server" button from use case screen. Remove matrix.org description. Add username availability to registration screen.
This commit is contained in:
parent
26bdc9af06
commit
ec8100383f
24 changed files with 224 additions and 208 deletions
|
@ -22,17 +22,17 @@
|
|||
|
||||
// MARK: Onboarding Authentication WIP
|
||||
"authentication_registration_title" = "Create your account";
|
||||
"authentication_registration_message" = "We’ll need some info to get you set up.";
|
||||
"authentication_registration_username" = "Username";
|
||||
"authentication_registration_username_footer" = "You can’t change this later";
|
||||
/* The placeholder will show the full Matrix ID that has been entered. */
|
||||
"authentication_registration_username_footer_available" = "Others can discover you %@";
|
||||
"authentication_registration_password_footer" = "Must be 8 characters or more";
|
||||
|
||||
"authentication_login_title" = "Welcome back!";
|
||||
"authentication_login_username" = "Username or Email";
|
||||
"authentication_login_forgot_password" = "Forgot password";
|
||||
|
||||
"authentication_server_info_title" = "Choose your server to store your data";
|
||||
"authentication_server_info_matrix_description" = "Join millions for free on the largest public server";
|
||||
"authentication_server_info_title" = "Where your conversations will live";
|
||||
|
||||
"authentication_server_selection_title" = "Choose your server";
|
||||
"authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data.";
|
||||
|
@ -46,6 +46,7 @@
|
|||
"authentication_verify_email_input_message" = "This will help verify your account and enables password recovery.";
|
||||
"authentication_verify_email_text_field_placeholder" = "Email Address";
|
||||
"authentication_verify_email_waiting_title" = "Check your email to verify.";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_verify_email_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
|
||||
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_verify_email_waiting_button" = "Resend email";
|
||||
|
@ -54,6 +55,7 @@
|
|||
"authentication_forgot_password_input_message" = "We will send you a verification link.";
|
||||
"authentication_forgot_password_text_field_placeholder" = "Email Address";
|
||||
"authentication_forgot_password_waiting_title" = "Check your email";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_forgot_password_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
|
||||
"authentication_forgot_password_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_forgot_password_waiting_button" = "Resend email";
|
||||
|
@ -69,6 +71,7 @@
|
|||
"authentication_verify_msisdn_text_field_placeholder" = "Phone Number";
|
||||
"authentication_verify_msisdn_otp_text_field_placeholder" = "Verification Code";
|
||||
"authentication_verify_msisdn_waiting_title" = "Confirm your phone number";
|
||||
/* The placeholder will show the phone number that was entered. */
|
||||
"authentication_verify_msisdn_waiting_message" = "We just sent a code to %@. Enter it below to verify it’s you.";
|
||||
"authentication_verify_msisdn_waiting_button" = "Resend code";
|
||||
"authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number";
|
||||
|
@ -82,7 +85,9 @@
|
|||
// MARK: Password Validation
|
||||
"password_validation_info_header" = "Your password should meet the criteria below:";
|
||||
"password_validation_error_header" = "Given password does not meet the criteria below:";
|
||||
/* The placeholder will show a number */
|
||||
"password_validation_error_min_length" = "At least %d characters.";
|
||||
/* The placeholder will show a number */
|
||||
"password_validation_error_max_length" = "Not exceed %d characters.";
|
||||
"password_validation_error_contain_lowercase_letter" = "Contain a lower-case letter.";
|
||||
"password_validation_error_contain_uppercase_letter" = "Contain an upper-case letter.";
|
||||
|
|
|
@ -114,8 +114,8 @@
|
|||
"onboarding_use_case_work_messaging" = "Teams";
|
||||
"onboarding_use_case_community_messaging" = "Communities";
|
||||
/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */
|
||||
"onboarding_use_case_not_sure_yet" = "Not sure yet? You can %@";
|
||||
"onboarding_use_case_skip_button" = "skip this question";
|
||||
"onboarding_use_case_not_sure_yet" = "Not sure yet? %@";
|
||||
"onboarding_use_case_skip_button" = "Skip this question";
|
||||
"onboarding_use_case_existing_server_message" = "Looking to join an existing server?";
|
||||
"onboarding_use_case_existing_server_button" = "Connect to server";
|
||||
|
||||
|
|
|
@ -4047,7 +4047,7 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingUseCaseMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_message")
|
||||
}
|
||||
/// Not sure yet? You can %@
|
||||
/// Not sure yet? %@
|
||||
public static func onboardingUseCaseNotSureYet(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_not_sure_yet", p1)
|
||||
}
|
||||
|
@ -4055,7 +4055,7 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingUseCasePersonalMessaging: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_personal_messaging")
|
||||
}
|
||||
/// skip this question
|
||||
/// Skip this question
|
||||
public static var onboardingUseCaseSkipButton: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_skip_button")
|
||||
}
|
||||
|
|
|
@ -78,10 +78,6 @@ public extension VectorL10n {
|
|||
static var authenticationRecaptchaMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_recaptcha_message")
|
||||
}
|
||||
/// We’ll need some info to get you set up.
|
||||
static var authenticationRegistrationMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_message")
|
||||
}
|
||||
/// Must be 8 characters or more
|
||||
static var authenticationRegistrationPasswordFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_password_footer")
|
||||
|
@ -98,11 +94,11 @@ public extension VectorL10n {
|
|||
static var authenticationRegistrationUsernameFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_username_footer")
|
||||
}
|
||||
/// Join millions for free on the largest public server
|
||||
static var authenticationServerInfoMatrixDescription: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_info_matrix_description")
|
||||
/// Others can discover you %@
|
||||
static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_username_footer_available", p1)
|
||||
}
|
||||
/// Choose your server to store your data
|
||||
/// Where your conversations will live
|
||||
static var authenticationServerInfoTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_info_title")
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
|
||||
enum EntryPoint {
|
||||
case registration
|
||||
case selectServerForRegistration
|
||||
case login
|
||||
}
|
||||
|
||||
|
@ -131,7 +130,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
}
|
||||
|
||||
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
|
||||
if initialScreen != .selectServerForRegistration {
|
||||
do {
|
||||
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
|
||||
try await authenticationService.startFlow(flow, for: homeserverAddress)
|
||||
|
@ -140,7 +138,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
displayError(message: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch initialScreen {
|
||||
case .registration:
|
||||
|
@ -149,8 +146,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
} else {
|
||||
showRegistrationScreen()
|
||||
}
|
||||
case .selectServerForRegistration:
|
||||
showServerSelectionScreen()
|
||||
case .login:
|
||||
if authenticationService.state.homeserver.needsLoginFallback {
|
||||
showFallback(for: flow)
|
||||
|
@ -312,6 +307,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
|
||||
// MARK: - Registration
|
||||
|
||||
#warning("Unused.")
|
||||
/// Pushes the server selection screen into the flow (other screens may also present it modally later).
|
||||
@MainActor private func showServerSelectionScreen() {
|
||||
MXLog.debug("[AuthenticationCoordinator] showServerSelectionScreen")
|
||||
|
@ -788,15 +784,9 @@ extension AuthenticationCoordinator: UIAdaptivePresentationControllerDelegate {
|
|||
|
||||
// MARK: - Unused conformances
|
||||
extension AuthenticationCoordinator {
|
||||
var customServerFieldsVisible: Bool {
|
||||
get { false }
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
func update(authenticationFlow: AuthenticationFlow) {
|
||||
// unused
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AuthFallBackViewControllerDelegate
|
||||
|
|
|
@ -36,9 +36,6 @@ enum AuthenticationCoordinatorResult {
|
|||
protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
|
||||
var callback: ((AuthenticationCoordinatorResult) -> Void)? { get set }
|
||||
|
||||
/// Whether the custom homeserver checkbox is enabled for the user to enter a homeserver URL.
|
||||
var customServerFieldsVisible: Bool { get set }
|
||||
|
||||
/// Update the screen to display registration or login.
|
||||
func update(authenticationFlow: AuthenticationFlow)
|
||||
|
||||
|
|
|
@ -48,13 +48,6 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((AuthenticationCoordinatorResult) -> Void)?
|
||||
|
||||
var customServerFieldsVisible = false {
|
||||
didSet {
|
||||
guard customServerFieldsVisible != oldValue else { return }
|
||||
authenticationViewController.setCustomServerFieldsVisible(customServerFieldsVisible)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: LegacyAuthenticationCoordinatorParameters) {
|
||||
|
|
|
@ -205,12 +205,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
if result == .customServer {
|
||||
beginAuthentication(with: .selectServerForRegistration, onStart: coordinator.stop)
|
||||
} else {
|
||||
beginAuthentication(with: .registration, onStart: coordinator.stop)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
|
@ -266,8 +262,6 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
coordinator.customServerFieldsVisible = useCaseResult == .customServer
|
||||
|
||||
authenticationCoordinator = coordinator
|
||||
|
||||
coordinator.start()
|
||||
|
@ -630,7 +624,7 @@ extension OnboardingSplashScreenViewModelResult {
|
|||
|
||||
extension OnboardingUseCaseViewModelResult {
|
||||
/// The result converted into the type stored in the user session.
|
||||
var userSessionPropertyValue: UserSessionProperties.UseCase? {
|
||||
var userSessionPropertyValue: UserSessionProperties.UseCase {
|
||||
switch self {
|
||||
case .personalMessaging:
|
||||
return .personalMessaging
|
||||
|
@ -640,8 +634,6 @@ extension OnboardingUseCaseViewModelResult {
|
|||
return .communityMessaging
|
||||
case .skipped:
|
||||
return .skipped
|
||||
case .customServer:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@ import Foundation
|
|||
struct AuthenticationHomeserverViewData: Equatable {
|
||||
/// The homeserver string to be shown to the user.
|
||||
let address: String
|
||||
/// Whether or not the homeserver is matrix.org.
|
||||
let isMatrixDotOrg: Bool
|
||||
/// Whether or not to display the username and password text fields during login.
|
||||
let showLoginForm: Bool
|
||||
/// Whether or not to display the username and password text fields during registration.
|
||||
|
@ -36,7 +34,6 @@ extension AuthenticationHomeserverViewData {
|
|||
/// A mock homeserver that is configured just like matrix.org.
|
||||
static var mockMatrixDotOrg: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "matrix.org",
|
||||
isMatrixDotOrg: true,
|
||||
showLoginForm: true,
|
||||
showRegistrationForm: true,
|
||||
ssoIdentityProviders: [
|
||||
|
@ -51,7 +48,6 @@ extension AuthenticationHomeserverViewData {
|
|||
/// A mock homeserver that supports login and registration via a password but has no SSO providers.
|
||||
static var mockBasicServer: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "example.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: true,
|
||||
showRegistrationForm: true,
|
||||
ssoIdentityProviders: [])
|
||||
|
@ -60,7 +56,6 @@ extension AuthenticationHomeserverViewData {
|
|||
/// A mock homeserver that supports only supports authentication via a single SSO provider.
|
||||
static var mockEnterpriseSSO: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "company.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: false,
|
||||
showRegistrationForm: false,
|
||||
ssoIdentityProviders: [SSOIdentityProvider(id: "test", name: "SAML", brand: nil, iconURL: nil)])
|
||||
|
@ -69,7 +64,6 @@ extension AuthenticationHomeserverViewData {
|
|||
/// A mock homeserver that supports only supports authentication via fallback.
|
||||
static var mockFallback: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "company.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: false,
|
||||
showRegistrationForm: false,
|
||||
ssoIdentityProviders: [])
|
||||
|
|
|
@ -27,7 +27,6 @@ struct AuthenticationServerInfoSection: View {
|
|||
// MARK: - Public
|
||||
|
||||
let address: String
|
||||
let showMatrixDotOrgInfo: Bool
|
||||
let editAction: () -> Void
|
||||
|
||||
// MARK: - Views
|
||||
|
@ -39,19 +38,10 @@ struct AuthenticationServerInfoSection: View {
|
|||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(address)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
if showMatrixDotOrgInfo {
|
||||
Text(VectorL10n.authenticationServerInfoMatrixDescription)
|
||||
.font(theme.fonts.caption1)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.accessibilityIdentifier("serverDescriptionText")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: editAction) {
|
||||
|
|
|
@ -65,7 +65,6 @@ struct AuthenticationState {
|
|||
/// The homeserver mapped into view data that is ready for display.
|
||||
var viewData: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: displayableAddress,
|
||||
isMatrixDotOrg: isMatrixDotOrg,
|
||||
showLoginForm: preferredLoginMode.supportsPasswordFlow,
|
||||
showRegistrationForm: registrationFlow != nil && !needsRegistrationFallback,
|
||||
ssoIdentityProviders: preferredLoginMode.ssoIdentityProviders ?? [])
|
||||
|
|
|
@ -32,12 +32,10 @@ class AuthenticationLoginUITests: MockScreenTest {
|
|||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
let state = "matrix.org"
|
||||
validateServerDescriptionIsVisible(for: state)
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
case .passwordOnly:
|
||||
let state = "a password only server"
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
|
||||
|
@ -47,7 +45,6 @@ class AuthenticationLoginUITests: MockScreenTest {
|
|||
validateNextButtonIsEnabled(for: state)
|
||||
case .ssoOnly:
|
||||
let state = "an SSO only server"
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
case .fallback:
|
||||
|
@ -56,20 +53,6 @@ class AuthenticationLoginUITests: MockScreenTest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks that the server description label is shown.
|
||||
func validateServerDescriptionIsVisible(for state: String) {
|
||||
let descriptionLabel = app.staticTexts["serverDescriptionText"]
|
||||
|
||||
XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).")
|
||||
XCTAssertEqual(descriptionLabel.label, VectorL10n.authenticationServerInfoMatrixDescription, "The server description should be correct for \(state).")
|
||||
}
|
||||
|
||||
/// Checks that the server description label is hidden.
|
||||
func validateServerDescriptionIsHidden(for state: String) {
|
||||
let descriptionLabel = app.staticTexts["serverDescriptionText"]
|
||||
XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).")
|
||||
}
|
||||
|
||||
/// Checks that the username and password text fields are shown along with the next button.
|
||||
func validateLoginFormIsVisible(for state: String) {
|
||||
let usernameTextField = app.textFields.element
|
||||
|
|
|
@ -37,15 +37,16 @@ struct AuthenticationLoginScreen: View {
|
|||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
.padding(.bottom, 28)
|
||||
|
||||
serverInfo
|
||||
.padding(.leading, 12)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Rectangle()
|
||||
.fill(theme.colors.quinaryContent)
|
||||
.frame(height: 1)
|
||||
.padding(.vertical, 21)
|
||||
.padding(.bottom, 22)
|
||||
|
||||
if viewModel.viewState.homeserver.showLoginForm {
|
||||
loginForm
|
||||
|
@ -86,8 +87,7 @@ struct AuthenticationLoginScreen: View {
|
|||
|
||||
/// The sever information section that includes a button to select a different server.
|
||||
var serverInfo: some View {
|
||||
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address,
|
||||
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
|
||||
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address) {
|
||||
viewModel.send(viewAction: .selectServer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,15 @@ enum AuthenticationRegistrationViewModelResult: CustomStringConvertible {
|
|||
// MARK: View
|
||||
|
||||
struct AuthenticationRegistrationViewState: BindableState {
|
||||
enum UsernameAvailability {
|
||||
/// The availability of the username is unknown.
|
||||
case unknown
|
||||
/// The username is available.
|
||||
case available
|
||||
/// The username is invalid for the following reason.
|
||||
case invalid(String)
|
||||
}
|
||||
|
||||
/// Data about the selected homeserver.
|
||||
var homeserver: AuthenticationHomeserverViewData
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
|
@ -63,12 +72,20 @@ struct AuthenticationRegistrationViewState: BindableState {
|
|||
/// This is used to delay showing an error state until the user has tried 1 password.
|
||||
var hasEditedPassword = false
|
||||
|
||||
/// An error message to be shown in the username text field footer.
|
||||
var usernameErrorMessage: String?
|
||||
/// The availability of the currently enetered username.
|
||||
var usernameAvailability: UsernameAvailability = .unknown
|
||||
|
||||
/// The message to show in the username text field footer.
|
||||
var usernameFooterMessage: String {
|
||||
usernameErrorMessage ?? VectorL10n.authenticationRegistrationUsernameFooter
|
||||
switch usernameAvailability {
|
||||
case .unknown:
|
||||
return VectorL10n.authenticationRegistrationUsernameFooter
|
||||
case .invalid(let errorMessage):
|
||||
return errorMessage
|
||||
case .available:
|
||||
let userID = "@\(bindings.username):\(homeserver.address)"
|
||||
return VectorL10n.authenticationRegistrationUsernameFooterAvailable(userID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show any SSO buttons.
|
||||
|
@ -76,19 +93,23 @@ struct AuthenticationRegistrationViewState: BindableState {
|
|||
!homeserver.ssoIdentityProviders.isEmpty
|
||||
}
|
||||
|
||||
/// Whether the current `username` is valid.
|
||||
var isUsernameValid: Bool {
|
||||
!bindings.username.isEmpty && usernameErrorMessage == nil
|
||||
/// Whether the current `username` is invalid.
|
||||
var isUsernameInvalid: Bool {
|
||||
if case .invalid = usernameAvailability {
|
||||
return true
|
||||
} else {
|
||||
return bindings.username.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the current `password` is valid.
|
||||
var isPasswordValid: Bool {
|
||||
bindings.password.count >= 8
|
||||
/// Whether the current `password` is invalid.
|
||||
var isPasswordInvalid: Bool {
|
||||
bindings.password.count < 8
|
||||
}
|
||||
|
||||
/// `true` if it is possible to continue, otherwise `false`.
|
||||
var hasValidCredentials: Bool {
|
||||
isUsernameValid && isPasswordValid
|
||||
!isUsernameInvalid && !isPasswordInvalid
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,8 +129,8 @@ enum AuthenticationRegistrationViewAction {
|
|||
case validateUsername
|
||||
/// Allows password validation to take place (sent after editing the password for the first time).
|
||||
case enablePasswordValidation
|
||||
/// Clear any errors being shown in the username text field footer.
|
||||
case clearUsernameError
|
||||
/// Clear any availability messages being shown in the username text field footer.
|
||||
case resetUsernameAvailability
|
||||
/// Continue using the input username and password.
|
||||
case next
|
||||
/// Continue using the supplied SSO provider.
|
||||
|
|
|
@ -48,8 +48,8 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
|
|||
Task { await validateUsername() }
|
||||
case .enablePasswordValidation:
|
||||
Task { await enablePasswordValidation() }
|
||||
case .clearUsernameError:
|
||||
Task { await clearUsernameError() }
|
||||
case .resetUsernameAvailability:
|
||||
Task { await resetUsernameAvailability() }
|
||||
case .next:
|
||||
Task { await callback?(.createAccount(username: state.bindings.username, password: state.bindings.password)) }
|
||||
case .continueWithSSO(let provider):
|
||||
|
@ -63,10 +63,15 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
|
|||
state.homeserver = homeserver
|
||||
}
|
||||
|
||||
@MainActor func confirmUsernameAvailability(_ username: String) {
|
||||
guard username == state.bindings.username else { return }
|
||||
state.usernameAvailability = .available
|
||||
}
|
||||
|
||||
@MainActor func displayError(_ type: AuthenticationRegistrationErrorType) {
|
||||
switch type {
|
||||
case .usernameUnavailable(let message):
|
||||
state.usernameErrorMessage = message
|
||||
state.usernameAvailability = .invalid(message)
|
||||
case .mxError(let message):
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: VectorL10n.error,
|
||||
|
@ -101,9 +106,9 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
|
|||
state.hasEditedPassword = true
|
||||
}
|
||||
|
||||
/// Clear any errors being shown in the username text field footer.
|
||||
@MainActor private func clearUsernameError() {
|
||||
guard state.usernameErrorMessage != nil else { return }
|
||||
state.usernameErrorMessage = nil
|
||||
/// Reset the username's availability, clearing any messages being shown in the username text field footer.
|
||||
@MainActor private func resetUsernameAvailability() {
|
||||
if case .unknown = state.usernameAvailability { return }
|
||||
state.usernameAvailability = .unknown
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,10 @@ protocol AuthenticationRegistrationViewModelProtocol {
|
|||
/// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`.
|
||||
@MainActor func update(homeserver: AuthenticationHomeserverViewData)
|
||||
|
||||
/// Update the view to confirm that the chosen username is available.
|
||||
/// - Parameter username: The username that was checked.
|
||||
@MainActor func confirmUsernameAvailability(_ username: String)
|
||||
|
||||
/// Display an error to the user.
|
||||
/// - Parameter type: The type of error to be displayed.
|
||||
@MainActor func displayError(_ type: AuthenticationRegistrationErrorType)
|
||||
|
|
|
@ -149,6 +149,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
|
|||
currentTask = Task {
|
||||
do {
|
||||
_ = try await registrationWizard.registrationAvailable(username: username)
|
||||
authenticationRegistrationViewModel.confirmUsernameAvailability(username)
|
||||
} catch {
|
||||
guard !Task.isCancelled, let mxError = MXError(nsError: error as NSError) else { return }
|
||||
if mxError.errcode == kMXErrCodeStringUserInUse
|
||||
|
|
|
@ -47,6 +47,7 @@ enum MockAuthenticationRegistrationScreenState: MockScreenState, CaseIterable {
|
|||
viewModel = AuthenticationRegistrationViewModel(homeserver: .mockBasicServer)
|
||||
viewModel.context.username = "alice"
|
||||
viewModel.context.password = "password"
|
||||
Task { await viewModel.confirmUsernameAvailability("alice") }
|
||||
case .passwordWithUsernameError:
|
||||
viewModel = AuthenticationRegistrationViewModel(homeserver: .mockBasicServer)
|
||||
viewModel.state.hasEditedUsername = true
|
||||
|
|
|
@ -36,7 +36,8 @@ class AuthenticationRegistrationUITests: MockScreenTest {
|
|||
validateSSOButtonsAreShown(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
validateUnknownUsernameAvailability(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
case .passwordOnly:
|
||||
let state = "a password only server"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
|
@ -45,7 +46,8 @@ class AuthenticationRegistrationUITests: MockScreenTest {
|
|||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
validateUnknownUsernameAvailability(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
case .passwordWithCredentials:
|
||||
let state = "a password only server with credentials entered"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
|
@ -54,7 +56,8 @@ class AuthenticationRegistrationUITests: MockScreenTest {
|
|||
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
validateUsernameAvailable(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
case .passwordWithUsernameError:
|
||||
let state = "a password only server with an invalid username"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
|
@ -147,15 +150,24 @@ class AuthenticationRegistrationUITests: MockScreenTest {
|
|||
XCTAssertEqual(usernameFooter.label, VectorL10n.authInvalidUserName, "The username footer should be showing an error for \(state).")
|
||||
}
|
||||
|
||||
/// Checks that neither the username or password text field footers are showing an error.
|
||||
func validateNoErrorsAreShown(for state: String) {
|
||||
func validateUsernameAvailable(for state: String) {
|
||||
let usernameFooter = textFieldFooter(for: "usernameTextField")
|
||||
XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).")
|
||||
XCTAssertTrue(usernameFooter.label.starts(with: VectorL10n.authenticationRegistrationUsernameFooterAvailable("")),
|
||||
"The username footer should be showing the username as available for \(state).")
|
||||
}
|
||||
|
||||
func validateUnknownUsernameAvailability(for state: String) {
|
||||
let usernameFooter = textFieldFooter(for: "usernameTextField")
|
||||
let passwordFooter = textFieldFooter(for: "passwordTextField")
|
||||
|
||||
XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).")
|
||||
XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).")
|
||||
XCTAssertEqual(usernameFooter.label, VectorL10n.authenticationRegistrationUsernameFooter,
|
||||
"The username footer should be showing the default message for \(state).")
|
||||
}
|
||||
|
||||
/// Checks that neither the username or password text field footers are showing an error.
|
||||
func validateNoPasswordErrorsAreShown(for state: String) {
|
||||
let passwordFooter = textFieldFooter(for: "passwordTextField")
|
||||
XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).")
|
||||
XCTAssertEqual(passwordFooter.label, VectorL10n.authenticationRegistrationPasswordFooter,
|
||||
"The password footer should be showing the default message for \(state).")
|
||||
}
|
||||
|
|
|
@ -63,40 +63,89 @@ import Combine
|
|||
}
|
||||
|
||||
func testUsernameError() async throws {
|
||||
// Given a form with a valid username.
|
||||
// Given a form with an entered username.
|
||||
context.username = "bob"
|
||||
XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error when the view model is created.")
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
|
||||
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid if there is no error.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
|
||||
|
||||
// When displaying the error as a username error.
|
||||
let errorMessage = "Username unavailable"
|
||||
viewModel.displayError(.usernameUnavailable(errorMessage))
|
||||
|
||||
// Then the error should be shown in the footer.
|
||||
XCTAssertEqual(context.viewState.usernameErrorMessage, errorMessage, "The error message should be stored.")
|
||||
guard case let .invalid(displayedError) = context.viewState.usernameAvailability else {
|
||||
XCTFail("The username should be invalid when an error is shown.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(displayedError, errorMessage, "The error message should match.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, errorMessage, "The error message should replace the standard footer message.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.")
|
||||
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.")
|
||||
|
||||
// When clearing the error.
|
||||
context.send(viewAction: .clearUsernameError)
|
||||
context.send(viewAction: .resetUsernameAvailability)
|
||||
|
||||
// Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors.
|
||||
try await Task.sleep(nanoseconds: 100_000_000)
|
||||
await Task.yield()
|
||||
|
||||
// Then the error should be hidden again.
|
||||
XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error anymore.")
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.")
|
||||
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when an error is cleared.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.")
|
||||
}
|
||||
|
||||
func testUsernameAvailability() async throws {
|
||||
// Given a form with an entered username.
|
||||
context.username = "bob"
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
|
||||
|
||||
// When updating the state for an available username
|
||||
viewModel.confirmUsernameAvailability("bob")
|
||||
|
||||
// Then the error should be shown in the footer.
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .available,
|
||||
"The username should be detected as available.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooterAvailable("@bob:matrix.org"),
|
||||
"The footer message should display that the username is available.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid,
|
||||
"The username should continue to be valid when it is available.")
|
||||
|
||||
// When clearing the error.
|
||||
context.send(viewAction: .resetUsernameAvailability)
|
||||
|
||||
// Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors.
|
||||
await Task.yield()
|
||||
|
||||
// Then the error should be hidden again.
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.")
|
||||
}
|
||||
|
||||
func testUsernameAvailabilityWhenChanged() async throws {
|
||||
// Given a form with an entered username.
|
||||
context.username = "robert"
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
|
||||
|
||||
// When updating the state for an available username that was previously entered.
|
||||
viewModel.confirmUsernameAvailability("bob")
|
||||
|
||||
// Then the username should not be shown as available.
|
||||
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should not be updated.")
|
||||
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should continue to be valid when unverified.")
|
||||
}
|
||||
|
||||
func testEmptyUsernameWithShortPassword() {
|
||||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
|
||||
// When entering a password of 7 characters without a username.
|
||||
|
@ -104,8 +153,8 @@ import Combine
|
|||
context.password = "1234567"
|
||||
|
||||
// Then the credentials should remain invalid.
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "A 7-character password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "A 7-character password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
}
|
||||
|
||||
|
@ -113,8 +162,8 @@ import Combine
|
|||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
|
||||
// When entering a password of 8 characters without a username.
|
||||
|
@ -122,8 +171,8 @@ import Combine
|
|||
context.password = "12345678"
|
||||
|
||||
// Then the password should be valid but the credentials should still be invalid.
|
||||
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
}
|
||||
|
||||
|
@ -131,8 +180,8 @@ import Combine
|
|||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
|
||||
// When entering a username without a password.
|
||||
|
@ -140,8 +189,8 @@ import Combine
|
|||
context.password = ""
|
||||
|
||||
// Then the username should be valid but the credentials should still be invalid.
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
}
|
||||
|
||||
|
@ -149,8 +198,8 @@ import Combine
|
|||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
|
||||
// When entering a username and password and encountering a username error
|
||||
|
@ -161,8 +210,8 @@ import Combine
|
|||
viewModel.displayError(.usernameUnavailable(errorMessage))
|
||||
|
||||
// Then the password should be valid but the credentials should still be invalid.
|
||||
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.")
|
||||
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
}
|
||||
|
||||
|
@ -170,8 +219,8 @@ import Combine
|
|||
// Given a form with an empty username and password.
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
|
||||
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
|
||||
// When entering a username and an 8-character password.
|
||||
|
@ -179,8 +228,24 @@ import Combine
|
|||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered valid.
|
||||
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
|
||||
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.")
|
||||
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationRegistrationViewState.UsernameAvailability: Equatable {
|
||||
public static func == (lhs: AuthenticationRegistrationViewState.UsernameAvailability,
|
||||
rhs: AuthenticationRegistrationViewState.UsernameAvailability) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.unknown, .unknown):
|
||||
return true
|
||||
case (.available, .available):
|
||||
return true
|
||||
case (.invalid, .invalid):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,15 +35,16 @@ struct AuthenticationRegistrationScreen: View {
|
|||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
.padding(.bottom, 28)
|
||||
|
||||
serverInfo
|
||||
.padding(.leading, 12)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Rectangle()
|
||||
.fill(theme.colors.quinaryContent)
|
||||
.frame(height: 1)
|
||||
.padding(.vertical, 21)
|
||||
.padding(.bottom, 22)
|
||||
|
||||
if viewModel.viewState.homeserver.showRegistrationForm {
|
||||
registrationForm
|
||||
|
@ -84,18 +85,12 @@ struct AuthenticationRegistrationScreen: View {
|
|||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationRegistrationMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
/// The sever information section that includes a button to select a different server.
|
||||
var serverInfo: some View {
|
||||
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address,
|
||||
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
|
||||
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address) {
|
||||
viewModel.send(viewAction: .selectServer)
|
||||
}
|
||||
}
|
||||
|
@ -107,21 +102,21 @@ struct AuthenticationRegistrationScreen: View {
|
|||
placeHolder: VectorL10n.authenticationRegistrationUsername,
|
||||
text: $viewModel.username,
|
||||
footerText: viewModel.viewState.usernameFooterMessage,
|
||||
isError: viewModel.viewState.hasEditedUsername && !viewModel.viewState.isUsernameValid,
|
||||
isError: viewModel.viewState.hasEditedUsername && viewModel.viewState.isUsernameInvalid,
|
||||
isFirstResponder: false,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .next,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no),
|
||||
onEditingChanged: usernameEditingChanged,
|
||||
onCommit: { isPasswordFocused = true })
|
||||
.onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .clearUsernameError) }
|
||||
.onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .resetUsernameAvailability) }
|
||||
.accessibilityIdentifier("usernameTextField")
|
||||
|
||||
RoundedBorderTextField(title: nil,
|
||||
placeHolder: VectorL10n.authPasswordPlaceholder,
|
||||
text: $viewModel.password,
|
||||
footerText: VectorL10n.authenticationRegistrationPasswordFooter,
|
||||
isError: viewModel.viewState.hasEditedPassword && !viewModel.viewState.isPasswordValid,
|
||||
isError: viewModel.viewState.hasEditedPassword && viewModel.viewState.isPasswordInvalid,
|
||||
isFirstResponder: isPasswordFocused,
|
||||
configuration: UIKitTextInputConfiguration(returnKeyType: .done,
|
||||
isSecureTextEntry: true),
|
||||
|
|
|
@ -57,9 +57,7 @@ final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
|
|||
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
|
||||
|
||||
// Show a loading indicator which can be dismissed externally by calling `stop`.
|
||||
if result != .customServer {
|
||||
self.startLoading()
|
||||
}
|
||||
self.completion?(result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ enum OnboardingUseCaseViewModelResult {
|
|||
case workMessaging
|
||||
case communityMessaging
|
||||
case skipped
|
||||
case customServer
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
|
|
@ -29,6 +29,23 @@ struct OnboardingUseCaseSelectionScreen: View {
|
|||
|
||||
@ObservedObject var viewModel: OnboardingUseCaseViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
titleContent
|
||||
.padding(.bottom, 36)
|
||||
|
||||
useCaseButtons
|
||||
}
|
||||
.readableFrame()
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
|
||||
/// The screen's title and instructions.
|
||||
var titleContent: some View {
|
||||
VStack(spacing: 8) {
|
||||
|
@ -76,47 +93,6 @@ struct OnboardingUseCaseSelectionScreen: View {
|
|||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
/// A footer showing a button to connect to a server.
|
||||
var serverFooter: some View {
|
||||
VStack(spacing: 14) {
|
||||
Text(VectorL10n.onboardingUseCaseExistingServerMessage)
|
||||
.font(theme.fonts.subheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
|
||||
Button { viewModel.send(viewAction: .answer(.customServer)) } label: {
|
||||
Text(VectorL10n.onboardingUseCaseExistingServerButton)
|
||||
.font(theme.fonts.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
titleContent
|
||||
.padding(.bottom, 36)
|
||||
|
||||
useCaseButtons
|
||||
}
|
||||
.readableFrame()
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
serverFooter
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
|
||||
}
|
||||
}
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
.accentColor(theme.colors.accent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
|
Loading…
Reference in a new issue