mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge branch 'develop' of github.com:vector-im/element-ios into langleyd/6419_remove_ffmpeg_again
This commit is contained in:
commit
7bec008538
147 changed files with 1414 additions and 1454 deletions
9
.github/workflows/triage-move-labelled.yml
vendored
9
.github/workflows/triage-move-labelled.yml
vendored
|
@ -44,14 +44,7 @@ jobs:
|
|||
name: P1 X-Needs-Design to Design project board
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
|
||||
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
||||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
|
||||
contains(github.event.issue.labels.*.name, 'S-Major') &&
|
||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||
contains(github.event.issue.labels.*.name, 'A11y') &&
|
||||
contains(github.event.issue.labels.*.name, 'O-Frequent'))
|
||||
contains(github.event.issue.labels.*.name, 'X-Needs-Design')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
|
|
18
CHANGES.md
18
CHANGES.md
|
@ -1,3 +1,21 @@
|
|||
## Changes in 1.8.23 (2022-07-15)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Reword account deactivation button on the Settings screen. ([#6436](https://github.com/vector-im/element-ios/issues/6436))
|
||||
|
||||
|
||||
## Changes in 1.8.22 (2022-07-13)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Upgrade MatrixSDK version ([v0.23.12](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.12)).
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- Fix a bug where the login screen is shown after choosing to create an account. ([#6417](https://github.com/vector-im/element-ios/pull/6417))
|
||||
|
||||
|
||||
## Changes in 1.8.21 (2022-07-12)
|
||||
|
||||
✨ Features
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.8.22
|
||||
CURRENT_PROJECT_VERSION = 1.8.22
|
||||
MARKETING_VERSION = 1.8.24
|
||||
CURRENT_PROJECT_VERSION = 1.8.24
|
||||
|
|
2
Podfile
2
Podfile
|
@ -16,7 +16,7 @@ use_frameworks!
|
|||
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixSDKVersion = '= 0.23.11'
|
||||
$matrixSDKVersion = '= 0.23.12'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
|
18
Podfile.lock
18
Podfile.lock
|
@ -56,9 +56,9 @@ PODS:
|
|||
- LoggerAPI (1.9.200):
|
||||
- Logging (~> 1.1)
|
||||
- Logging (1.4.0)
|
||||
- MatrixSDK (0.23.11):
|
||||
- MatrixSDK/Core (= 0.23.11)
|
||||
- MatrixSDK/Core (0.23.11):
|
||||
- MatrixSDK (0.23.12):
|
||||
- MatrixSDK/Core (= 0.23.12)
|
||||
- MatrixSDK/Core (0.23.12):
|
||||
- AFNetworking (~> 4.0.0)
|
||||
- GZIP (~> 1.3.0)
|
||||
- libbase58 (~> 0.1.4)
|
||||
|
@ -66,9 +66,9 @@ PODS:
|
|||
- OLMKit (~> 3.2.5)
|
||||
- Realm (= 10.27.0)
|
||||
- SwiftyBeaver (= 1.9.5)
|
||||
- MatrixSDK/CryptoSDK (0.23.11):
|
||||
- MatrixSDK/CryptoSDK (0.23.12):
|
||||
- MatrixSDKCrypto (= 0.1.0)
|
||||
- MatrixSDK/JingleCallStack (0.23.11):
|
||||
- MatrixSDK/JingleCallStack (0.23.12):
|
||||
- JitsiMeetSDK (= 5.0.2)
|
||||
- MatrixSDK/Core
|
||||
- MatrixSDKCrypto (0.1.0)
|
||||
|
@ -123,8 +123,8 @@ DEPENDENCIES:
|
|||
- KeychainAccess (~> 4.2.2)
|
||||
- KTCenterFlowLayout (~> 1.3.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatrixSDK (= 0.23.11)
|
||||
- MatrixSDK/JingleCallStack (= 0.23.11)
|
||||
- MatrixSDK (= 0.23.12)
|
||||
- MatrixSDK/JingleCallStack (= 0.23.12)
|
||||
- OLMKit
|
||||
- PostHog (~> 1.4.4)
|
||||
- ReadMoreTextView (~> 3.0.1)
|
||||
|
@ -221,7 +221,7 @@ SPEC CHECKSUMS:
|
|||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
MatrixSDK: 8b02aafb65c798a96c55007b1ae8e9ad4a5453e3
|
||||
MatrixSDK: cfc3316cbe8534ff9d4d7b49a1bc92dfb6b1296d
|
||||
MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e
|
||||
OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5
|
||||
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
|
||||
|
@ -241,6 +241,6 @@ SPEC CHECKSUMS:
|
|||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: ff5148bd99a1b2cacc3ba3e430afe267a74b7092
|
||||
PODFILE CHECKSUM: c5629e0a7affba9c508e7c30572ada2200ab725d
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -19,72 +19,3 @@
|
|||
// MARK: Onboarding Personalization WIP
|
||||
|
||||
"image_picker_action_files" = "Choose from files";
|
||||
|
||||
// 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";
|
||||
"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_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.";
|
||||
"authentication_server_selection_server_url" = "Server URL";
|
||||
"authentication_server_selection_server_footer" = "You can only connect to a server that has already been set up";
|
||||
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
|
||||
|
||||
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
|
||||
|
||||
"authentication_verify_email_input_title" = "Enter your email address";
|
||||
"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.";
|
||||
"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";
|
||||
|
||||
"authentication_forgot_password_input_title" = "Enter your email address";
|
||||
"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";
|
||||
"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";
|
||||
|
||||
"authentication_choose_password_input_title" = "Choose a new password";
|
||||
"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more.";
|
||||
"authentication_choose_password_text_field_placeholder" = "New Password";
|
||||
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
|
||||
"authentication_choose_password_submit_button" = "Reset Password";
|
||||
|
||||
"authentication_verify_msisdn_input_title" = "Enter your phone number";
|
||||
"authentication_verify_msisdn_input_message" = "This will help verify your account and enables password recovery.";
|
||||
"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";
|
||||
"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";
|
||||
|
||||
"authentication_terms_title" = "Privacy policy";
|
||||
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
|
||||
"authentication_terms_policy_url_error" = "Unable to find the selected policy. Please try again later.";
|
||||
|
||||
"authentication_recaptcha_message" = "This server would like to make sure you are not a robot";
|
||||
|
||||
// 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:";
|
||||
"password_validation_error_min_length" = "At least %d characters.";
|
||||
"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.";
|
||||
"password_validation_error_contain_number" = "Contain a number.";
|
||||
"password_validation_error_contain_symbol" = "Contain a symbol.";
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
"accessibility_checkbox_label" = "checkbox";
|
||||
"accessibility_button_label" = "button";
|
||||
|
||||
// Onboarding
|
||||
// MARK: Onboarding
|
||||
"onboarding_splash_register_button_title" = "Create account";
|
||||
"onboarding_splash_login_button_title" = "I already have an account";
|
||||
"onboarding_splash_page_1_title" = "Own your conversations.";
|
||||
|
@ -109,19 +109,19 @@
|
|||
"onboarding_splash_page_4_message" = "Element is also great for the workplace. It’s trusted by the world’s most secure organisations.";
|
||||
|
||||
"onboarding_use_case_title" = "Who will you chat to the most?";
|
||||
"onboarding_use_case_message" = "We’ll help you get connected.";
|
||||
"onboarding_use_case_message" = "We’ll help you get connected";
|
||||
"onboarding_use_case_personal_messaging" = "Friends and family";
|
||||
"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";
|
||||
|
||||
"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";
|
||||
|
||||
|
@ -135,14 +135,93 @@
|
|||
"onboarding_display_name_max_length" = "Your display name must be less than 256 characters";
|
||||
|
||||
"onboarding_avatar_title" = "Add a profile picture";
|
||||
"onboarding_avatar_message" = "You can change this anytime.";
|
||||
"onboarding_avatar_message" = "Time to put a face to the name";
|
||||
"onboarding_avatar_accessibility_label" = "Profile picture";
|
||||
|
||||
"onboarding_celebration_title" = "You’re all set!";
|
||||
"onboarding_celebration_message" = "Your preferences have been saved.";
|
||||
"onboarding_celebration_title" = "Looking good!";
|
||||
"onboarding_celebration_message" = "Head to settings anytime to update your profile";
|
||||
"onboarding_celebration_button" = "Let's go";
|
||||
|
||||
// Authentication
|
||||
// MARK: Authentication
|
||||
"authentication_registration_title" = "Create your account";
|
||||
"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_server_info_title" = "Where your conversations will live";
|
||||
|
||||
"authentication_login_title" = "Welcome back!";
|
||||
"authentication_login_username" = "Username / Email / Phone";
|
||||
"authentication_login_forgot_password" = "Forgot password";
|
||||
"authentication_server_info_title_login" = "Where your conversations live";
|
||||
|
||||
"authentication_server_selection_login_title" = "Connect to homeserver";
|
||||
"authentication_server_selection_login_message" = "What is the address of your server?";
|
||||
"authentication_server_selection_register_title" = "Select your homeserver";
|
||||
"authentication_server_selection_register_message" = "What is the address of your server? This is like a home for all your data";
|
||||
"authentication_server_selection_server_url" = "Homeserver URL";
|
||||
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
|
||||
|
||||
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
|
||||
|
||||
"authentication_verify_email_input_title" = "Enter your email";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_verify_email_input_message" = "%@ needs to verify your account";
|
||||
"authentication_verify_email_text_field_placeholder" = "Email";
|
||||
"authentication_verify_email_waiting_title" = "Verify your email.";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_verify_email_waiting_message" = "Follow the instructions sent to %@";
|
||||
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_verify_email_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_forgot_password_input_title" = "Enter your email";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_forgot_password_input_message" = "%@ will send you a verification link";
|
||||
"authentication_forgot_password_text_field_placeholder" = "Email";
|
||||
"authentication_forgot_password_waiting_title" = "Check your email.";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_forgot_password_waiting_message" = "Follow the instructions sent to %@";
|
||||
"authentication_forgot_password_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_choose_password_input_title" = "Choose a new password";
|
||||
"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more";
|
||||
"authentication_choose_password_text_field_placeholder" = "New Password";
|
||||
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
|
||||
"authentication_choose_password_submit_button" = "Reset Password";
|
||||
|
||||
"authentication_verify_msisdn_input_title" = "Enter your phone number";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_verify_msisdn_input_message" = "%@ needs to verify your account";
|
||||
"authentication_verify_msisdn_text_field_placeholder" = "Phone Number";
|
||||
"authentication_verify_msisdn_otp_text_field_placeholder" = "Confirmation Code";
|
||||
"authentication_verify_msisdn_waiting_title" = "Verify your phone number";
|
||||
/* The placeholder will show the phone number that was entered. */
|
||||
"authentication_verify_msisdn_waiting_message" = "A code was sent to %@";
|
||||
"authentication_verify_msisdn_waiting_button" = "Resend code";
|
||||
"authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number";
|
||||
|
||||
"authentication_terms_title" = "Server policies";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_terms_message" = "Please read %@’s terms and policies";
|
||||
"authentication_terms_policy_url_error" = "Unable to find the selected policy. Please try again later.";
|
||||
|
||||
"authentication_recaptcha_title" = "Are you a human?";
|
||||
|
||||
// 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.";
|
||||
"password_validation_error_contain_number" = "Contain a number.";
|
||||
"password_validation_error_contain_symbol" = "Contain a symbol.";
|
||||
|
||||
|
||||
// MARK: Legacy Authentication
|
||||
"auth_login" = "Log in";
|
||||
"auth_register" = "Register";
|
||||
"auth_submit" = "Submit";
|
||||
|
@ -706,7 +785,7 @@ Tap the + to start adding people.";
|
|||
"settings_crypto_export" = "Export keys";
|
||||
"settings_crypto_blacklist_unverified_devices" = "Encrypt to verified sessions only";
|
||||
|
||||
"settings_deactivate_my_account" = "Deactivate my account";
|
||||
"settings_deactivate_my_account" = "Deactivate account permanently";
|
||||
|
||||
"settings_key_backup_info" = "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.";
|
||||
"settings_key_backup_info_checking" = "Checking…";
|
||||
|
@ -2445,6 +2524,7 @@ To enable access, tap Settings> Location and select Always";
|
|||
"message_reply_to_sender_sent_a_voice_message" = "sent a voice message.";
|
||||
"message_reply_to_sender_sent_a_file" = "sent a file.";
|
||||
"message_reply_to_sender_sent_their_location" = "has shared their location.";
|
||||
"message_reply_to_sender_sent_their_live_location" = "Live location.";
|
||||
"message_reply_to_message_to_reply_to_prefix" = "In reply to";
|
||||
|
||||
// Room members
|
||||
|
|
|
@ -531,6 +531,194 @@ public class VectorL10n: NSObject {
|
|||
public static var authenticatedSessionFlowNotSupported: String {
|
||||
return VectorL10n.tr("Vector", "authenticated_session_flow_not_supported")
|
||||
}
|
||||
/// Your account is not created yet. Stop the registration process?
|
||||
public static var authenticationCancelFlowConfirmationMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_cancel_flow_confirmation_message")
|
||||
}
|
||||
/// Make sure it’s 8 characters or more
|
||||
public static var authenticationChoosePasswordInputMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_input_message")
|
||||
}
|
||||
/// Choose a new password
|
||||
public static var authenticationChoosePasswordInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_input_title")
|
||||
}
|
||||
/// Sign out of all devices
|
||||
public static var authenticationChoosePasswordSignoutAllDevices: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_signout_all_devices")
|
||||
}
|
||||
/// Reset Password
|
||||
public static var authenticationChoosePasswordSubmitButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_submit_button")
|
||||
}
|
||||
/// New Password
|
||||
public static var authenticationChoosePasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_text_field_placeholder")
|
||||
}
|
||||
/// %@ will send you a verification link
|
||||
public static func authenticationForgotPasswordInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_input_message", p1)
|
||||
}
|
||||
/// Enter your email
|
||||
public static var authenticationForgotPasswordInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_input_title")
|
||||
}
|
||||
/// Email
|
||||
public static var authenticationForgotPasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
public static var authenticationForgotPasswordWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_button")
|
||||
}
|
||||
/// Follow the instructions sent to %@
|
||||
public static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_message", p1)
|
||||
}
|
||||
/// Check your email.
|
||||
public static var authenticationForgotPasswordWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_title")
|
||||
}
|
||||
/// Forgot password
|
||||
public static var authenticationLoginForgotPassword: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_forgot_password")
|
||||
}
|
||||
/// Welcome back!
|
||||
public static var authenticationLoginTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_title")
|
||||
}
|
||||
/// Username / Email / Phone
|
||||
public static var authenticationLoginUsername: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_username")
|
||||
}
|
||||
/// Are you a human?
|
||||
public static var authenticationRecaptchaTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_recaptcha_title")
|
||||
}
|
||||
/// Must be 8 characters or more
|
||||
public static var authenticationRegistrationPasswordFooter: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_password_footer")
|
||||
}
|
||||
/// Create your account
|
||||
public static var authenticationRegistrationTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_title")
|
||||
}
|
||||
/// Username
|
||||
public static var authenticationRegistrationUsername: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username")
|
||||
}
|
||||
/// You can’t change this later
|
||||
public static var authenticationRegistrationUsernameFooter: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username_footer")
|
||||
}
|
||||
/// Others can discover you %@
|
||||
public static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username_footer_available", p1)
|
||||
}
|
||||
/// Where your conversations will live
|
||||
public static var authenticationServerInfoTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_info_title")
|
||||
}
|
||||
/// Where your conversations live
|
||||
public static var authenticationServerInfoTitleLogin: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_info_title_login")
|
||||
}
|
||||
/// Cannot find a server at this URL, please check it is correct.
|
||||
public static var authenticationServerSelectionGenericError: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_generic_error")
|
||||
}
|
||||
/// What is the address of your server?
|
||||
public static var authenticationServerSelectionLoginMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_login_message")
|
||||
}
|
||||
/// Connect to homeserver
|
||||
public static var authenticationServerSelectionLoginTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_login_title")
|
||||
}
|
||||
/// What is the address of your server? This is like a home for all your data
|
||||
public static var authenticationServerSelectionRegisterMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_register_message")
|
||||
}
|
||||
/// Select your homeserver
|
||||
public static var authenticationServerSelectionRegisterTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_register_title")
|
||||
}
|
||||
/// Homeserver URL
|
||||
public static var authenticationServerSelectionServerUrl: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_server_url")
|
||||
}
|
||||
/// Please read %@’s terms and policies
|
||||
public static func authenticationTermsMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_message", p1)
|
||||
}
|
||||
/// Unable to find the selected policy. Please try again later.
|
||||
public static var authenticationTermsPolicyUrlError: String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_policy_url_error")
|
||||
}
|
||||
/// Server policies
|
||||
public static var authenticationTermsTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_title")
|
||||
}
|
||||
/// %@ needs to verify your account
|
||||
public static func authenticationVerifyEmailInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_input_message", p1)
|
||||
}
|
||||
/// Enter your email
|
||||
public static var authenticationVerifyEmailInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_input_title")
|
||||
}
|
||||
/// Email
|
||||
public static var authenticationVerifyEmailTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
public static var authenticationVerifyEmailWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
public static var authenticationVerifyEmailWaitingHint: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_hint")
|
||||
}
|
||||
/// Follow the instructions sent to %@
|
||||
public static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_message", p1)
|
||||
}
|
||||
/// Verify your email.
|
||||
public static var authenticationVerifyEmailWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_title")
|
||||
}
|
||||
/// %@ needs to verify your account
|
||||
public static func authenticationVerifyMsisdnInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_message", p1)
|
||||
}
|
||||
/// Enter your phone number
|
||||
public static var authenticationVerifyMsisdnInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_title")
|
||||
}
|
||||
/// Invalid phone number
|
||||
public static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_invalid_phone_number")
|
||||
}
|
||||
/// Confirmation Code
|
||||
public static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_otp_text_field_placeholder")
|
||||
}
|
||||
/// Phone Number
|
||||
public static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_text_field_placeholder")
|
||||
}
|
||||
/// Resend code
|
||||
public static var authenticationVerifyMsisdnWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_button")
|
||||
}
|
||||
/// A code was sent to %@
|
||||
public static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_message", p1)
|
||||
}
|
||||
/// Verify your phone number
|
||||
public static var authenticationVerifyMsisdnWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_title")
|
||||
}
|
||||
/// Back
|
||||
public static var back: String {
|
||||
return VectorL10n.tr("Vector", "back")
|
||||
|
@ -3231,6 +3419,10 @@ public class VectorL10n: NSObject {
|
|||
public static var messageReplyToSenderSentAnImage: String {
|
||||
return VectorL10n.tr("Vector", "message_reply_to_sender_sent_an_image")
|
||||
}
|
||||
/// Live location.
|
||||
public static var messageReplyToSenderSentTheirLiveLocation: String {
|
||||
return VectorL10n.tr("Vector", "message_reply_to_sender_sent_their_live_location")
|
||||
}
|
||||
/// has shared their location.
|
||||
public static var messageReplyToSenderSentTheirLocation: String {
|
||||
return VectorL10n.tr("Vector", "message_reply_to_sender_sent_their_location")
|
||||
|
@ -3927,7 +4119,7 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingAvatarAccessibilityLabel: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_avatar_accessibility_label")
|
||||
}
|
||||
/// You can change this anytime.
|
||||
/// Time to put a face to the name
|
||||
public static var onboardingAvatarMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_avatar_message")
|
||||
}
|
||||
|
@ -3939,11 +4131,11 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingCelebrationButton: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_button")
|
||||
}
|
||||
/// Your preferences have been saved.
|
||||
/// Head to settings anytime to update your profile
|
||||
public static var onboardingCelebrationMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_message")
|
||||
}
|
||||
/// You’re all set!
|
||||
/// Looking good!
|
||||
public static var onboardingCelebrationTitle: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_title")
|
||||
}
|
||||
|
@ -3951,7 +4143,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)
|
||||
}
|
||||
|
@ -4043,11 +4235,11 @@ public class VectorL10n: NSObject {
|
|||
public static var onboardingUseCaseExistingServerMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_existing_server_message")
|
||||
}
|
||||
/// We’ll help you get connected.
|
||||
/// We’ll help you get connected
|
||||
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 +4247,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")
|
||||
}
|
||||
|
@ -4075,6 +4267,38 @@ public class VectorL10n: NSObject {
|
|||
public static var or: String {
|
||||
return VectorL10n.tr("Vector", "or")
|
||||
}
|
||||
/// Contain a lower-case letter.
|
||||
public static var passwordValidationErrorContainLowercaseLetter: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_contain_lowercase_letter")
|
||||
}
|
||||
/// Contain a number.
|
||||
public static var passwordValidationErrorContainNumber: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_contain_number")
|
||||
}
|
||||
/// Contain a symbol.
|
||||
public static var passwordValidationErrorContainSymbol: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_contain_symbol")
|
||||
}
|
||||
/// Contain an upper-case letter.
|
||||
public static var passwordValidationErrorContainUppercaseLetter: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_contain_uppercase_letter")
|
||||
}
|
||||
/// Given password does not meet the criteria below:
|
||||
public static var passwordValidationErrorHeader: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_header")
|
||||
}
|
||||
/// Not exceed %d characters.
|
||||
public static func passwordValidationErrorMaxLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_max_length", p1)
|
||||
}
|
||||
/// At least %d characters.
|
||||
public static func passwordValidationErrorMinLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Vector", "password_validation_error_min_length", p1)
|
||||
}
|
||||
/// Your password should meet the criteria below:
|
||||
public static var passwordValidationInfoHeader: String {
|
||||
return VectorL10n.tr("Vector", "password_validation_info_header")
|
||||
}
|
||||
/// CONVERSATIONS
|
||||
public static var peopleConversationSection: String {
|
||||
return VectorL10n.tr("Vector", "people_conversation_section")
|
||||
|
@ -6647,7 +6871,7 @@ public class VectorL10n: NSObject {
|
|||
public static var settingsDeactivateAccount: String {
|
||||
return VectorL10n.tr("Vector", "settings_deactivate_account")
|
||||
}
|
||||
/// Deactivate my account
|
||||
/// Deactivate account permanently
|
||||
public static var settingsDeactivateMyAccount: String {
|
||||
return VectorL10n.tr("Vector", "settings_deactivate_my_account")
|
||||
}
|
||||
|
|
|
@ -10,230 +10,10 @@ import Foundation
|
|||
|
||||
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
|
||||
public extension VectorL10n {
|
||||
/// Your account is not created yet. Stop the registration process?
|
||||
static var authenticationCancelFlowConfirmationMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_cancel_flow_confirmation_message")
|
||||
}
|
||||
/// Make sure it’s 8 characters or more.
|
||||
static var authenticationChoosePasswordInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_message")
|
||||
}
|
||||
/// Choose a new password
|
||||
static var authenticationChoosePasswordInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_title")
|
||||
}
|
||||
/// Sign out of all devices
|
||||
static var authenticationChoosePasswordSignoutAllDevices: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_signout_all_devices")
|
||||
}
|
||||
/// Reset Password
|
||||
static var authenticationChoosePasswordSubmitButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_submit_button")
|
||||
}
|
||||
/// New Password
|
||||
static var authenticationChoosePasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_text_field_placeholder")
|
||||
}
|
||||
/// We will send you a verification link.
|
||||
static var authenticationForgotPasswordInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_message")
|
||||
}
|
||||
/// Enter your email address
|
||||
static var authenticationForgotPasswordInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_title")
|
||||
}
|
||||
/// Email Address
|
||||
static var authenticationForgotPasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
static var authenticationForgotPasswordWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
static var authenticationForgotPasswordWaitingHint: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_hint")
|
||||
}
|
||||
/// To confirm your email address, tap the button in the email we just sent to %@
|
||||
static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_message", p1)
|
||||
}
|
||||
/// Check your email
|
||||
static var authenticationForgotPasswordWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_title")
|
||||
}
|
||||
/// Forgot password
|
||||
static var authenticationLoginForgotPassword: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_forgot_password")
|
||||
}
|
||||
/// Welcome back!
|
||||
static var authenticationLoginTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_title")
|
||||
}
|
||||
/// Username or Email
|
||||
static var authenticationLoginUsername: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_username")
|
||||
}
|
||||
/// This server would like to make sure you are not a robot
|
||||
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")
|
||||
}
|
||||
/// Create your account
|
||||
static var authenticationRegistrationTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_title")
|
||||
}
|
||||
/// Username
|
||||
static var authenticationRegistrationUsername: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_username")
|
||||
}
|
||||
/// You can’t change this later
|
||||
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")
|
||||
}
|
||||
/// Choose your server to store your data
|
||||
static var authenticationServerInfoTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_info_title")
|
||||
}
|
||||
/// Cannot find a server at this URL, please check it is correct.
|
||||
static var authenticationServerSelectionGenericError: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_generic_error")
|
||||
}
|
||||
/// What is the address of your server? A server is like a home for all your data.
|
||||
static var authenticationServerSelectionMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_message")
|
||||
}
|
||||
/// You can only connect to a server that has already been set up
|
||||
static var authenticationServerSelectionServerFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_footer")
|
||||
}
|
||||
/// Server URL
|
||||
static var authenticationServerSelectionServerUrl: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_url")
|
||||
}
|
||||
/// Choose your server
|
||||
static var authenticationServerSelectionTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_title")
|
||||
}
|
||||
/// Please read through T&C. You must accept in order to continue.
|
||||
static var authenticationTermsMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_message")
|
||||
}
|
||||
/// Unable to find the selected policy. Please try again later.
|
||||
static var authenticationTermsPolicyUrlError: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_policy_url_error")
|
||||
}
|
||||
/// Privacy policy
|
||||
static var authenticationTermsTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_title")
|
||||
}
|
||||
/// This will help verify your account and enables password recovery.
|
||||
static var authenticationVerifyEmailInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
|
||||
}
|
||||
/// Enter your email address
|
||||
static var authenticationVerifyEmailInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_title")
|
||||
}
|
||||
/// Email Address
|
||||
static var authenticationVerifyEmailTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
static var authenticationVerifyEmailWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
static var authenticationVerifyEmailWaitingHint: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_hint")
|
||||
}
|
||||
/// To confirm your email address, tap the button in the email we just sent to %@
|
||||
static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_message", p1)
|
||||
}
|
||||
/// Check your email to verify.
|
||||
static var authenticationVerifyEmailWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_title")
|
||||
}
|
||||
/// This will help verify your account and enables password recovery.
|
||||
static var authenticationVerifyMsisdnInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_message")
|
||||
}
|
||||
/// Enter your phone number
|
||||
static var authenticationVerifyMsisdnInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_title")
|
||||
}
|
||||
/// Invalid phone number
|
||||
static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_invalid_phone_number")
|
||||
}
|
||||
/// Verification Code
|
||||
static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_otp_text_field_placeholder")
|
||||
}
|
||||
/// Phone Number
|
||||
static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_text_field_placeholder")
|
||||
}
|
||||
/// Resend code
|
||||
static var authenticationVerifyMsisdnWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_button")
|
||||
}
|
||||
/// We just sent a code to %@. Enter it below to verify it’s you.
|
||||
static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1)
|
||||
}
|
||||
/// Confirm your phone number
|
||||
static var authenticationVerifyMsisdnWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_title")
|
||||
}
|
||||
/// Choose from files
|
||||
static var imagePickerActionFiles: String {
|
||||
return VectorL10n.tr("Untranslated", "image_picker_action_files")
|
||||
}
|
||||
/// Contain a lower-case letter.
|
||||
static var passwordValidationErrorContainLowercaseLetter: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_lowercase_letter")
|
||||
}
|
||||
/// Contain a number.
|
||||
static var passwordValidationErrorContainNumber: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_number")
|
||||
}
|
||||
/// Contain a symbol.
|
||||
static var passwordValidationErrorContainSymbol: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_symbol")
|
||||
}
|
||||
/// Contain an upper-case letter.
|
||||
static var passwordValidationErrorContainUppercaseLetter: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_uppercase_letter")
|
||||
}
|
||||
/// Given password does not meet the criteria below:
|
||||
static var passwordValidationErrorHeader: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_header")
|
||||
}
|
||||
/// Not exceed %d characters.
|
||||
static func passwordValidationErrorMaxLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_max_length", p1)
|
||||
}
|
||||
/// At least %d characters.
|
||||
static func passwordValidationErrorMinLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_min_length", p1)
|
||||
}
|
||||
/// Your password should meet the criteria below:
|
||||
static var passwordValidationInfoHeader: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_info_header")
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ UINavigationControllerDelegate
|
|||
@param session The matrix session.
|
||||
@return Indicate NO if the key verification screen could not be presented.
|
||||
*/
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
|
||||
inSession:(MXSession*)session;
|
||||
|
||||
- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession;
|
||||
|
|
|
@ -3636,11 +3636,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
return;
|
||||
}
|
||||
|
||||
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<MXKeyVerificationTransaction *> * _Nonnull transactions) {
|
||||
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<id<MXKeyVerificationTransaction>> * _Nonnull transactions) {
|
||||
|
||||
MXLogDebug(@"[AppDelegate][MXKeyVerification] checkPendingIncomingKeyVerificationsInSession: transactions: %@", transactions);
|
||||
|
||||
for (MXKeyVerificationTransaction *transaction in transactions)
|
||||
for (id<MXKeyVerificationTransaction> transaction in transactions)
|
||||
{
|
||||
if (transaction.isIncoming)
|
||||
{
|
||||
|
@ -3664,7 +3664,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
}
|
||||
}
|
||||
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
|
||||
inSession:(MXSession*)session
|
||||
{
|
||||
BOOL presented = NO;
|
||||
|
@ -3810,7 +3810,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
|
||||
MXKeyVerificationRequest *keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
|
||||
id<MXKeyVerificationRequest> keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
|
||||
|
||||
if ([keyVerificationRequest isKindOfClass:MXKeyVerificationByDMRequest.class])
|
||||
{
|
||||
|
@ -3893,7 +3893,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
|||
- (void)presentNewKeyVerificationRequestAlertForSession:(MXSession*)session
|
||||
senderName:(NSString*)senderName
|
||||
senderId:(NSString*)senderId
|
||||
request:(MXKeyVerificationRequest*)keyVerificationRequest
|
||||
request:(id<MXKeyVerificationRequest>)keyVerificationRequest
|
||||
{
|
||||
if (keyVerificationRequest.state != MXKeyVerificationRequestStatePending)
|
||||
{
|
||||
|
|
|
@ -32,7 +32,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
|
||||
enum EntryPoint {
|
||||
case registration
|
||||
case selectServerForRegistration
|
||||
case login
|
||||
}
|
||||
|
||||
|
@ -131,15 +130,13 @@ 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)
|
||||
} catch {
|
||||
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
|
||||
displayError(message: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
|
||||
try await authenticationService.startFlow(flow, for: homeserverAddress)
|
||||
} catch {
|
||||
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
|
||||
displayError(message: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
switch initialScreen {
|
||||
|
@ -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")
|
||||
|
@ -398,7 +394,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
@MainActor private func showVerifyEmailScreen(registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showVerifyEmailScreen")
|
||||
|
||||
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard)
|
||||
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationVerifyEmailCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
|
@ -416,11 +413,10 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
@MainActor private func showTermsScreen(terms: MXLoginTerms?, registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showTermsScreen")
|
||||
|
||||
let homeserver = authenticationService.state.homeserver
|
||||
let localizedPolicies = terms?.policiesData(forLanguage: Bundle.mxk_language(), defaultLanguage: Bundle.mxk_fallbackLanguage())
|
||||
let parameters = AuthenticationTermsCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
localizedPolicies: localizedPolicies ?? [],
|
||||
homeserverAddress: homeserver.displayableAddress)
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationTermsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
|
@ -463,7 +459,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
|||
@MainActor private func showVerifyMSISDNScreen(registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showVerifyMSISDNScreen")
|
||||
|
||||
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard)
|
||||
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationVerifyMsisdnCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
|
@ -788,15 +785,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) {
|
||||
|
@ -78,7 +71,16 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
|
|||
authenticationViewController.authVCDelegate = self
|
||||
// Set (or clear) any soft-logout credentials.
|
||||
authenticationViewController.softLogoutCredentials = authenticationService.softLogoutCredentials
|
||||
// Listen for changes from deep links.
|
||||
|
||||
// Configure custom servers if already customised by a deep link.
|
||||
let homeserver = authenticationService.state.homeserver.address
|
||||
let identityServer = authenticationService.state.identityServer
|
||||
if homeserver != BuildSettings.serverConfigDefaultHomeserverUrlString
|
||||
|| (identityServer != nil && identityServer != BuildSettings.serverConfigDefaultIdentityServerUrlString) {
|
||||
authenticationViewController.showCustomHomeserver(homeserver, andIdentityServer: identityServer)
|
||||
}
|
||||
|
||||
// Listen for further changes from deep links.
|
||||
AuthenticationService.shared.delegate = self
|
||||
}
|
||||
|
|
@ -899,18 +899,21 @@
|
|||
// This is required before updating view's textfields (homeserver url...)
|
||||
[self loadViewIfNeeded];
|
||||
|
||||
// Force register mode
|
||||
self.authType = MXKAuthenticationTypeLogin;
|
||||
if (softLogoutCredentials)
|
||||
{
|
||||
// Force register mode
|
||||
self.authType = MXKAuthenticationTypeLogin;
|
||||
|
||||
[self setHomeServerTextFieldText:softLogoutCredentials.homeServer];
|
||||
[self setIdentityServerTextFieldText:softLogoutCredentials.identityServer];
|
||||
[self setHomeServerTextFieldText:softLogoutCredentials.homeServer];
|
||||
[self setIdentityServerTextFieldText:softLogoutCredentials.identityServer];
|
||||
|
||||
// Cancel potential request in progress
|
||||
[mxCurrentOperation cancel];
|
||||
mxCurrentOperation = nil;
|
||||
// Cancel potential request in progress
|
||||
[mxCurrentOperation cancel];
|
||||
mxCurrentOperation = nil;
|
||||
|
||||
// Remove the current auth inputs view
|
||||
self.authInputsView = nil;
|
||||
// Remove the current auth inputs view
|
||||
self.authInputsView = nil;
|
||||
}
|
||||
|
||||
// Set parameters and trigger a refresh (the parameters will be taken into account during [handleAuthenticationSession:])
|
||||
_softLogoutCredentials = softLogoutCredentials;
|
||||
|
|
|
@ -40,6 +40,10 @@ class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalize
|
|||
func senderSentTheirLocation() -> String {
|
||||
return VectorL10n.messageReplyToSenderSentTheirLocation
|
||||
}
|
||||
|
||||
func senderSentTheirLiveLocation() -> String {
|
||||
return VectorL10n.messageReplyToSenderSentTheirLiveLocation
|
||||
}
|
||||
|
||||
func messageToReplyToPrefix() -> String {
|
||||
return VectorL10n.messageReplyToMessageToReplyToPrefix
|
||||
|
|
|
@ -205,11 +205,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
if result == .customServer {
|
||||
beginAuthentication(with: .selectServerForRegistration, onStart: coordinator.stop)
|
||||
} else {
|
||||
beginAuthentication(with: .registration, onStart: coordinator.stop)
|
||||
}
|
||||
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()
|
||||
|
@ -572,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.
|
||||
|
@ -630,7 +627,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 +637,6 @@ extension OnboardingUseCaseViewModelResult {
|
|||
return .communityMessaging
|
||||
case .skipped:
|
||||
return .skipped
|
||||
case .customServer:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -678,7 +678,7 @@ const CGFloat kTypingCellHeight = 24;
|
|||
return roomBubbleCellData;
|
||||
}
|
||||
|
||||
- (MXKeyVerificationRequest*)keyVerificationRequestFromEventId:(NSString*)eventId
|
||||
- (id<MXKeyVerificationRequest>)keyVerificationRequestFromEventId:(NSString*)eventId
|
||||
{
|
||||
RoomBubbleCellData *roomBubbleCellData = [self roomBubbleCellDataForEventId:eventId];
|
||||
|
||||
|
@ -745,7 +745,7 @@ const CGFloat kTypingCellHeight = 24;
|
|||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *notification)
|
||||
{
|
||||
MXKeyVerificationTransaction *keyVerificationTransaction = (MXKeyVerificationTransaction*)notification.object;
|
||||
id<MXKeyVerificationTransaction> keyVerificationTransaction = (id<MXKeyVerificationTransaction>)notification.object;
|
||||
|
||||
if ([keyVerificationTransaction.dmRoomId isEqualToString:self.roomId])
|
||||
{
|
||||
|
@ -927,7 +927,7 @@ const CGFloat kTypingCellHeight = 24;
|
|||
|
||||
- (void)acceptVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure
|
||||
{
|
||||
MXKeyVerificationRequest *keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
id<MXKeyVerificationRequest> keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
|
||||
if (!keyVerificationRequest)
|
||||
{
|
||||
|
@ -950,7 +950,7 @@ const CGFloat kTypingCellHeight = 24;
|
|||
|
||||
- (void)declineVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure
|
||||
{
|
||||
MXKeyVerificationRequest *keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
id<MXKeyVerificationRequest> keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
|
||||
if (!keyVerificationRequest)
|
||||
{
|
||||
|
|
|
@ -223,6 +223,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
|||
placeholderBackground.isHidden = bannerViewData.showMap
|
||||
placeholderBackground.image = placeholderBackgroundImage
|
||||
mapView.isHidden = !bannerViewData.showMap
|
||||
attributionLabel.isHidden = !bannerViewData.showMap
|
||||
|
||||
switch bannerViewData.status {
|
||||
case .starting:
|
||||
|
@ -237,7 +238,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
|||
|
||||
private func liveLocationBannerViewData(from viewState: TimelineLiveLocationViewState) -> TimelineLiveLocationViewData {
|
||||
|
||||
var status: LiveLocationSharingStatus
|
||||
let status: LiveLocationSharingStatus
|
||||
let iconTint: UIColor
|
||||
let title: String
|
||||
var titleColor: UIColor = theme.colors.primaryContent
|
||||
|
|
|
@ -3683,7 +3683,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart)
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
|
||||
|
@ -3719,7 +3720,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart)
|
||||
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
|
@ -6769,7 +6771,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
||||
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
|
||||
|
||||
BOOL result = (event.eventType != MXEventTypePollStart && (!attachment || attachment.type != MXKAttachmentTypeSticker));
|
||||
BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker;
|
||||
|
||||
if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
|
||||
{
|
||||
|
@ -6795,6 +6797,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
|||
case MXEventTypeKeyVerificationMac:
|
||||
case MXEventTypeKeyVerificationDone:
|
||||
case MXEventTypeKeyVerificationCancel:
|
||||
case MXEventTypePollStart:
|
||||
case MXEventTypeBeaconInfo:
|
||||
result = NO;
|
||||
break;
|
||||
case MXEventTypeCustom:
|
||||
|
|
|
@ -130,6 +130,17 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room
|
|||
super.prepareForReuse()
|
||||
self.event = nil
|
||||
}
|
||||
|
||||
override func onLongPressGesture(_ longPressGestureRecognizer: UILongPressGestureRecognizer!) {
|
||||
|
||||
var userInfo: [String: Any]?
|
||||
|
||||
if let event = self.event {
|
||||
userInfo = [kMXKRoomBubbleCellEventKey: event]
|
||||
}
|
||||
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellLongPressOnEvent, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocationPlainCell: RoomTimelineLocationViewDelegate {
|
||||
|
|
|
@ -617,7 +617,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
|||
{
|
||||
Section *sectionDeactivate = [Section sectionWithTag:SECTION_TAG_DEACTIVATE_ACCOUNT];
|
||||
[sectionDeactivate addRowWithTag:0];
|
||||
sectionDeactivate.headerTitle = [VectorL10n settingsDeactivateMyAccount];
|
||||
sectionDeactivate.headerTitle = [VectorL10n settingsDeactivateAccount];
|
||||
[tmpSections addObject:sectionDeactivate];
|
||||
}
|
||||
|
||||
|
|
|
@ -17,46 +17,32 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AnalyticsPromptUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAnalyticsPromptScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AnalyticsPromptUITests(selector: #selector(verifyAnalyticsPromptScreen))
|
||||
}
|
||||
|
||||
func verifyAnalyticsPromptScreen() throws {
|
||||
guard let screenState = screenState as? MockAnalyticsPromptScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .promptType(let promptType):
|
||||
verifyAnalyticsPromptType(promptType)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that the prompt is displayed correctly for new users compared to upgrading from Matomo
|
||||
func verifyAnalyticsPromptType(_ promptType: AnalyticsPromptType) {
|
||||
class AnalyticsPromptUITests: MockScreenTestCase {
|
||||
/// Verify that the prompt is displayed correctly for new users.
|
||||
func testAnalyticsPromptNewUser() {
|
||||
app.goToScreenWithIdentifier(MockAnalyticsPromptScreenState.promptType(.newUser).title)
|
||||
|
||||
let enableButton = app.buttons["enableButton"]
|
||||
let disableButton = app.buttons["disableButton"]
|
||||
|
||||
XCTAssert(enableButton.exists)
|
||||
XCTAssert(disableButton.exists)
|
||||
|
||||
switch promptType {
|
||||
case .newUser:
|
||||
XCTAssertEqual(enableButton.label, VectorL10n.enable)
|
||||
XCTAssertEqual(disableButton.label, VectorL10n.locationSharingInvalidAuthorizationNotNow)
|
||||
case .upgrade:
|
||||
XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes)
|
||||
XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop)
|
||||
}
|
||||
XCTAssertEqual(enableButton.label, VectorL10n.enable)
|
||||
XCTAssertEqual(disableButton.label, VectorL10n.locationSharingInvalidAuthorizationNotNow)
|
||||
}
|
||||
|
||||
func verifyAnalyticsPromptLongName(name: String) {
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssertEqual(displayNameText.label, name)
|
||||
|
||||
/// Verify that the prompt is displayed correctly for when upgrading from Matomo.
|
||||
func testAnalyticsPromptUpgrade() {
|
||||
app.goToScreenWithIdentifier(MockAnalyticsPromptScreenState.promptType(.upgrade).title)
|
||||
|
||||
let enableButton = app.buttons["enableButton"]
|
||||
let disableButton = app.buttons["disableButton"]
|
||||
|
||||
XCTAssert(enableButton.exists)
|
||||
XCTAssert(disableButton.exists)
|
||||
|
||||
XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes)
|
||||
XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,31 +17,10 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationChoosePasswordScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationChoosePasswordUITests(selector: #selector(verifyAuthenticationChoosePasswordScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationChoosePasswordScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationChoosePasswordScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredInvalidPassword:
|
||||
verifyEnteredInvalidPassword()
|
||||
case .enteredValidPassword:
|
||||
verifyEnteredValidPassword()
|
||||
case .enteredValidPasswordAndSignoutAllDevicesChecked:
|
||||
verifyEnteredValidPasswordAndSignoutAllDevicesChecked()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
class AuthenticationChoosePasswordUITests: MockScreenTestCase {
|
||||
func testEmptyPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.emptyPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
|
@ -58,7 +37,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
|||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredInvalidPassword() {
|
||||
func testEnteredInvalidPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredInvalidPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
|
@ -75,7 +56,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
|||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPassword() {
|
||||
func testEnteredValidPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
|
@ -92,7 +75,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
|||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPasswordAndSignoutAllDevicesChecked() {
|
||||
func testEnteredValidPasswordAndSignoutAllDevicesChecked() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPasswordAndSignoutAllDevicesChecked.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
|
|
|
@ -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,30 +27,25 @@ struct AuthenticationServerInfoSection: View {
|
|||
// MARK: - Public
|
||||
|
||||
let address: String
|
||||
let showMatrixDotOrgInfo: Bool
|
||||
let flow: AuthenticationFlow
|
||||
let editAction: () -> Void
|
||||
|
||||
var title: String {
|
||||
flow == .login ? VectorL10n.authenticationServerInfoTitleLogin : VectorL10n.authenticationServerInfoTitle
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(VectorL10n.authenticationServerInfoTitle)
|
||||
Text(title)
|
||||
.font(theme.fonts.subheadline)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
Text(address)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 ?? [])
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -32,11 +32,18 @@ enum AuthenticationForgotPasswordViewModelResult {
|
|||
// MARK: View
|
||||
|
||||
struct AuthenticationForgotPasswordViewState: BindableState {
|
||||
/// The homeserver that the user is using to reset their password.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// An email has been sent and the app is waiting for the user to tap the link.
|
||||
var hasSentEmail = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationForgotPasswordBindings
|
||||
|
||||
/// The message shown in the header while asking for an email address to be entered.
|
||||
var formHeaderMessage: String {
|
||||
VectorL10n.authenticationForgotPasswordInputMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether the email address is valid and the user can continue.
|
||||
var hasInvalidAddress: Bool {
|
||||
bindings.emailAddress.isEmpty
|
||||
|
|
|
@ -31,8 +31,9 @@ class AuthenticationForgotPasswordViewModel: AuthenticationForgotPasswordViewMod
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(emailAddress: String = "") {
|
||||
let viewState = AuthenticationForgotPasswordViewState(bindings: AuthenticationForgotPasswordBindings(emailAddress: emailAddress))
|
||||
init(homeserver: AuthenticationHomeserverViewData, emailAddress: String = "") {
|
||||
let viewState = AuthenticationForgotPasswordViewState(homeserver: homeserver,
|
||||
bindings: AuthenticationForgotPasswordBindings(emailAddress: emailAddress))
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ import CommonKit
|
|||
struct AuthenticationForgotPasswordCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let loginWizard: LoginWizard
|
||||
/// The homeserver currently being used.
|
||||
let homeserver: AuthenticationState.Homeserver
|
||||
}
|
||||
|
||||
enum AuthenticationForgotPasswordCoordinatorResult {
|
||||
|
@ -63,7 +65,7 @@ final class AuthenticationForgotPasswordCoordinator: Coordinator, Presentable {
|
|||
@MainActor init(parameters: AuthenticationForgotPasswordCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AuthenticationForgotPasswordViewModel()
|
||||
let viewModel = AuthenticationForgotPasswordViewModel(homeserver: parameters.homeserver.viewData)
|
||||
let view = AuthenticationForgotPasswordScreen(viewModel: viewModel.context)
|
||||
authenticationForgotPasswordViewModel = viewModel
|
||||
authenticationForgotPasswordHostingController = VectorHostingController(rootView: view)
|
||||
|
|
|
@ -37,11 +37,14 @@ enum MockAuthenticationForgotPasswordScreenState: MockScreenState, CaseIterable
|
|||
let viewModel: AuthenticationForgotPasswordViewModel
|
||||
switch self {
|
||||
case .emptyAddress:
|
||||
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "")
|
||||
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "")
|
||||
case .enteredAddress:
|
||||
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "test@example.com")
|
||||
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "test@example.com")
|
||||
case .hasSentEmail:
|
||||
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "test@example.com")
|
||||
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "test@example.com")
|
||||
Task { await viewModel.updateForSentEmail() }
|
||||
}
|
||||
|
||||
|
|
|
@ -17,35 +17,17 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationForgotPasswordUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationForgotPasswordScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationForgotPasswordUITests(selector: #selector(verifyAuthenticationForgotPasswordScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationForgotPasswordScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationForgotPasswordScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyAddress:
|
||||
verifyEmptyAddress()
|
||||
case .enteredAddress:
|
||||
verifyEnteredAddress()
|
||||
case .hasSentEmail:
|
||||
verifyWaitingForEmailLink()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyAddress() {
|
||||
class AuthenticationForgotPasswordUITests: MockScreenTestCase {
|
||||
func testEmptyAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.emptyAddress.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
|
||||
|
||||
let addressTextField = app.textFields["addressTextField"]
|
||||
XCTAssertTrue(addressTextField.exists, "The text field should be shown before an email is sent.")
|
||||
XCTAssertEqual(addressTextField.value as? String, "Email Address", "The text field should be showing the placeholder before text is input.")
|
||||
XCTAssertEqual(addressTextField.value as? String, VectorL10n.authenticationForgotPasswordTextFieldPlaceholder,
|
||||
"The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let nextButton = app.buttons["nextButton"]
|
||||
XCTAssertTrue(nextButton.exists, "The next button should be shown before an email is sent.")
|
||||
|
@ -65,7 +47,9 @@ class AuthenticationForgotPasswordUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyEnteredAddress() {
|
||||
func testEnteredAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.enteredAddress.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
|
||||
|
||||
|
@ -91,7 +75,9 @@ class AuthenticationForgotPasswordUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyWaitingForEmailLink() {
|
||||
func testWaitingForEmailLink() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.hasSentEmail.title)
|
||||
|
||||
XCTAssertFalse(app.staticTexts["titleLabel"].exists, "The title should be hidden once an email has been sent.")
|
||||
XCTAssertFalse(app.staticTexts["messageLabel"].exists, "The message should be hidden once an email has been sent.")
|
||||
XCTAssertFalse(app.textFields["addressTextField"].exists, "The text field should be hidden once an email has been sent.")
|
||||
|
|
|
@ -24,7 +24,7 @@ class AuthenticationForgotPasswordViewModelTests: XCTestCase {
|
|||
var context: AuthenticationForgotPasswordViewModelType.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = AuthenticationForgotPasswordViewModel()
|
||||
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ struct AuthenticationForgotPasswordForm: View {
|
|||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(VectorL10n.authenticationForgotPasswordInputMessage)
|
||||
Text(viewModel.viewState.formHeaderMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -260,7 +260,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
|
|||
let modalRouter = NavigationRouter()
|
||||
|
||||
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
|
||||
loginWizard: loginWizard)
|
||||
loginWizard: loginWizard,
|
||||
homeserver: parameters.authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
|
|
|
@ -17,57 +17,45 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationLoginUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationLoginScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationLoginUITests(selector: #selector(verifyAuthenticationLoginScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationLoginScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationLoginScreenState else { fatalError("no screen") }
|
||||
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)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
case .passwordWithCredentials:
|
||||
let state = "a password only server with credentials entered"
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
case .ssoOnly:
|
||||
let state = "an SSO only server"
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
case .fallback:
|
||||
let state = "a fallback server"
|
||||
validateFallback(for: state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the server description label is shown.
|
||||
func validateServerDescriptionIsVisible(for state: String) {
|
||||
let descriptionLabel = app.staticTexts["serverDescriptionText"]
|
||||
class AuthenticationLoginUITests: MockScreenTestCase {
|
||||
func testMatrixDotOrg() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.matrixDotOrg.title)
|
||||
|
||||
XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).")
|
||||
XCTAssertEqual(descriptionLabel.label, VectorL10n.authenticationServerInfoMatrixDescription, "The server description should be correct for \(state).")
|
||||
let state = "matrix.org"
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(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).")
|
||||
func testPasswordOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordOnly.title)
|
||||
|
||||
let state = "a password only server"
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
}
|
||||
|
||||
func testPasswordWithCredentials() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordWithCredentials.title)
|
||||
|
||||
let state = "a password only server with credentials entered"
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
}
|
||||
|
||||
func testSSOOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.ssoOnly.title)
|
||||
|
||||
let state = "an SSO only server"
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
}
|
||||
|
||||
func testFallback() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.fallback.title)
|
||||
|
||||
let state = "a fallback server"
|
||||
validateFallback(for: state)
|
||||
}
|
||||
|
||||
/// Checks that the username and password text fields are shown along with the next button.
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
@ -87,7 +88,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) {
|
||||
flow: .login) {
|
||||
viewModel.send(viewAction: .selectServer)
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +105,7 @@ struct AuthenticationLoginScreen: View {
|
|||
onEditingChanged: usernameEditingChanged,
|
||||
onCommit: { isPasswordFocused = true })
|
||||
.accessibilityIdentifier("usernameTextField")
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
.padding(.bottom, 7)
|
||||
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.authPasswordPlaceholder,
|
||||
text: $viewModel.password,
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationReCaptchaUITests: MockScreenTest {
|
||||
class AuthenticationReCaptchaUITests: MockScreenTestCase {
|
||||
// Nothing to test as the view only has a single state.
|
||||
}
|
||||
|
|
|
@ -62,15 +62,10 @@ struct AuthenticationReCaptchaScreen: View {
|
|||
OnboardingIconImage(image: Asset.Images.onboardingCongratulationsIcon)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(VectorL10n.authenticationRegistrationTitle)
|
||||
Text(VectorL10n.authenticationRecaptchaTitle)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationRecaptchaMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -50,8 +50,19 @@ 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
|
||||
/// 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.
|
||||
|
@ -63,12 +74,22 @@ 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:
|
||||
// https is never shown to the user but http is, so strip the scheme.
|
||||
let domain = homeserver.address.replacingOccurrences(of: "http://", with: "")
|
||||
let userID = "@\(bindings.username):\(domain)"
|
||||
return VectorL10n.authenticationRegistrationUsernameFooterAvailable(userID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show any SSO buttons.
|
||||
|
@ -76,19 +97,28 @@ 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
|
||||
}
|
||||
|
||||
/// `true` if valid credentials have been entered and the homeserver is loaded.
|
||||
var canSubmit: Bool {
|
||||
hasValidCredentials && !isLoading
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,8 +138,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):
|
||||
|
@ -59,14 +59,29 @@ 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
|
||||
}
|
||||
|
||||
@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 +116,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,22 @@ 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)
|
||||
|
||||
/// Display an error to the user.
|
||||
/// - Parameter type: The type of error to be displayed.
|
||||
@MainActor func displayError(_ type: AuthenticationRegistrationErrorType)
|
||||
|
|
|
@ -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
|
||||
|
@ -149,6 +189,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
|
||||
|
@ -244,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,66 +17,78 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationRegistrationUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationRegistrationScreenState.self
|
||||
class AuthenticationRegistrationUITests: MockScreenTestCase {
|
||||
func testMatrixDotOrg() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.matrixDotOrg.title)
|
||||
|
||||
let state = "matrix.org"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateUnknownUsernameAvailability(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationRegistrationUITests(selector: #selector(verifyAuthenticationRegistrationScreen))
|
||||
|
||||
func testPasswordOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordOnly.title)
|
||||
|
||||
let state = "a password only server"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
|
||||
validateUnknownUsernameAvailability(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
}
|
||||
|
||||
func verifyAuthenticationRegistrationScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationRegistrationScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
let state = "matrix.org"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
case .passwordOnly:
|
||||
let state = "a password only server"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
case .passwordWithCredentials:
|
||||
let state = "a password only server with credentials entered"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
|
||||
validateNoErrorsAreShown(for: state)
|
||||
case .passwordWithUsernameError:
|
||||
let state = "a password only server with an invalid username"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
|
||||
validateUsernameError(for: state)
|
||||
case .ssoOnly:
|
||||
let state = "an SSO only server"
|
||||
validateRegistrationFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
case .fallback:
|
||||
let state = "fallback"
|
||||
validateRegistrationFormIsHidden(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsShown(for: state)
|
||||
}
|
||||
|
||||
func testPasswordWithCredentials() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordWithCredentials.title)
|
||||
|
||||
let state = "a password only server with credentials entered"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
|
||||
validateUsernameAvailable(for: state)
|
||||
validateNoPasswordErrorsAreShown(for: state)
|
||||
}
|
||||
|
||||
func testPasswordWithUsernameError() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordWithUsernameError.title)
|
||||
|
||||
let state = "a password only server with an invalid username"
|
||||
validateRegistrationFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
validateUsernameError(for: state)
|
||||
}
|
||||
|
||||
func testSSOOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.ssoOnly.title)
|
||||
|
||||
let state = "an SSO only server"
|
||||
validateRegistrationFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
validateFallbackButtonIsHidden(for: state)
|
||||
}
|
||||
|
||||
func testFallback() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.fallback.title)
|
||||
|
||||
let state = "fallback"
|
||||
validateRegistrationFormIsHidden(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
validateFallbackButtonIsShown(for: state)
|
||||
}
|
||||
|
||||
|
||||
/// Checks that the username and password text fields are shown along with the next button.
|
||||
func validateRegistrationFormIsVisible(for state: String) {
|
||||
let usernameTextField = app.textFields.element
|
||||
|
@ -147,15 +159,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,63 @@ 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.")
|
||||
}
|
||||
|
||||
@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 {
|
||||
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,13 @@ 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) {
|
||||
flow: .register) {
|
||||
viewModel.send(viewAction: .selectServer)
|
||||
}
|
||||
}
|
||||
|
@ -107,21 +103,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),
|
||||
|
@ -133,7 +129,7 @@ struct AuthenticationRegistrationScreen: View {
|
|||
Text(VectorL10n.next)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.hasValidCredentials)
|
||||
.disabled(!viewModel.viewState.canSubmit)
|
||||
.accessibilityIdentifier("nextButton")
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +173,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)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,12 +32,17 @@ struct AuthenticationServerSelectionViewState: BindableState {
|
|||
var bindings: AuthenticationServerSelectionBindings
|
||||
/// An error message to be shown in the text field footer.
|
||||
var footerErrorMessage: String?
|
||||
/// The flow that the screen is being used for.
|
||||
let flow: AuthenticationFlow
|
||||
/// Whether the screen is presented modally or within a navigation stack.
|
||||
var hasModalPresentation: Bool
|
||||
|
||||
/// The message to show in the text field footer.
|
||||
var footerMessage: String {
|
||||
footerErrorMessage ?? VectorL10n.authenticationServerSelectionServerFooter
|
||||
var headerTitle: String {
|
||||
flow == .login ? VectorL10n.authenticationServerSelectionLoginTitle : VectorL10n.authenticationServerSelectionRegisterTitle
|
||||
}
|
||||
|
||||
var headerMessage: String {
|
||||
flow == .login ? VectorL10n.authenticationServerSelectionLoginMessage : VectorL10n.authenticationServerSelectionRegisterMessage
|
||||
}
|
||||
|
||||
/// The title shown on the confirm button.
|
||||
|
|
|
@ -32,9 +32,10 @@ class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewM
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(homeserverAddress: String, hasModalPresentation: Bool) {
|
||||
init(homeserverAddress: String, flow: AuthenticationFlow, hasModalPresentation: Bool) {
|
||||
let bindings = AuthenticationServerSelectionBindings(homeserverAddress: homeserverAddress)
|
||||
super.init(initialViewState: AuthenticationServerSelectionViewState(bindings: bindings,
|
||||
flow: flow,
|
||||
hasModalPresentation: hasModalPresentation))
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
|
|||
|
||||
let homeserver = parameters.authenticationService.state.homeserver
|
||||
let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.displayableAddress,
|
||||
flow: parameters.authenticationService.state.flow,
|
||||
hasModalPresentation: parameters.hasModalPresentation)
|
||||
let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context)
|
||||
authenticationServerSelectionViewModel = viewModel
|
||||
|
|
|
@ -26,6 +26,7 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
|
|||
case matrix
|
||||
case emptyAddress
|
||||
case invalidAddress
|
||||
case login
|
||||
case nonModal
|
||||
|
||||
/// The associated screen
|
||||
|
@ -39,16 +40,24 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
|
|||
switch self {
|
||||
case .matrix:
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
|
||||
flow: .register,
|
||||
hasModalPresentation: true)
|
||||
case .emptyAddress:
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "",
|
||||
flow: .register,
|
||||
hasModalPresentation: true)
|
||||
case .invalidAddress:
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "thisisbad",
|
||||
flow: .register,
|
||||
hasModalPresentation: true)
|
||||
Task { await viewModel.displayError(.footerMessage(VectorL10n.errorCommonMessage)) }
|
||||
case .login:
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
|
||||
flow: .login,
|
||||
hasModalPresentation: true)
|
||||
case .nonModal:
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
|
||||
flow: .register,
|
||||
hasModalPresentation: false)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,31 +17,15 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationServerSelectionUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationServerSelectionScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationServerSelectionUITests(selector: #selector(verifyAuthenticationServerSelectionScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationServerSelectionScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationServerSelectionScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrix:
|
||||
verifyNormalState()
|
||||
case .emptyAddress:
|
||||
verifyEmptyAddress()
|
||||
case .invalidAddress:
|
||||
verifyInvalidAddress()
|
||||
case .nonModal:
|
||||
verifyNonModalPresentation()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyNormalState() {
|
||||
class AuthenticationServerSelectionUITests: MockScreenTestCase {
|
||||
func testRegisterState() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.matrix.title)
|
||||
|
||||
let title = app.staticTexts["headerTitle"]
|
||||
XCTAssertEqual(title.label, VectorL10n.authenticationServerSelectionRegisterTitle)
|
||||
let message = app.staticTexts["headerMessage"]
|
||||
XCTAssertEqual(message.label, VectorL10n.authenticationServerSelectionRegisterMessage)
|
||||
|
||||
let serverTextField = app.textFields.element
|
||||
XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org as passed to the view model init.")
|
||||
|
||||
|
@ -51,14 +35,25 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
|
|||
XCTAssertTrue(confirmButton.isEnabled, "The confirm button should be enabled when there is an address.")
|
||||
|
||||
let textFieldFooter = app.staticTexts["textFieldFooter"]
|
||||
XCTAssertTrue(textFieldFooter.exists)
|
||||
XCTAssertEqual(textFieldFooter.label, VectorL10n.authenticationServerSelectionServerFooter)
|
||||
XCTAssertFalse(textFieldFooter.exists, "The footer shouldn't be shown when there isn't an error.")
|
||||
|
||||
let dismissButton = app.buttons["dismissButton"]
|
||||
XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.")
|
||||
}
|
||||
|
||||
func verifyEmptyAddress() {
|
||||
|
||||
func testLoginState() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.login.title)
|
||||
|
||||
let title = app.staticTexts["headerTitle"]
|
||||
XCTAssertEqual(title.label, VectorL10n.authenticationServerSelectionLoginTitle)
|
||||
let message = app.staticTexts["headerMessage"]
|
||||
XCTAssertEqual(message.label, VectorL10n.authenticationServerSelectionLoginMessage)
|
||||
}
|
||||
|
||||
func testEmptyAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.emptyAddress.title)
|
||||
|
||||
let serverTextField = app.textFields.element
|
||||
XCTAssertEqual(serverTextField.value as? String, VectorL10n.authenticationServerSelectionServerUrl, "The text field should show placeholder text in this state.")
|
||||
|
||||
|
@ -67,7 +62,9 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
|
|||
XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when the address is empty.")
|
||||
}
|
||||
|
||||
func verifyInvalidAddress() {
|
||||
func testInvalidAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.invalidAddress.title)
|
||||
|
||||
let serverTextField = app.textFields.element
|
||||
XCTAssertEqual(serverTextField.value as? String, "thisisbad", "The text field should show the entered server.")
|
||||
|
||||
|
@ -80,7 +77,9 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
|
|||
XCTAssertEqual(textFieldFooter.label, VectorL10n.errorCommonMessage)
|
||||
}
|
||||
|
||||
func verifyNonModalPresentation() {
|
||||
func testNonModalPresentation() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.nonModal.title)
|
||||
|
||||
let dismissButton = app.buttons["dismissButton"]
|
||||
XCTAssertFalse(dismissButton.exists, "The dismiss button should be hidden when not in modal presentation.")
|
||||
|
||||
|
|
|
@ -27,14 +27,14 @@ class AuthenticationServerSelectionViewModelTests: XCTestCase {
|
|||
var context: AuthenticationServerSelectionViewModelType.Context!
|
||||
|
||||
override func setUp() {
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "", hasModalPresentation: true)
|
||||
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "", flow: .login, hasModalPresentation: true)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
@MainActor func testErrorMessage() async throws {
|
||||
// Given a new instance of the view model.
|
||||
XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.")
|
||||
XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown.")
|
||||
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error shown.")
|
||||
|
||||
// When an error occurs.
|
||||
let message = "Unable to contact server."
|
||||
|
@ -42,16 +42,16 @@ class AuthenticationServerSelectionViewModelTests: XCTestCase {
|
|||
|
||||
// Then the footer should now be showing an error.
|
||||
XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.")
|
||||
XCTAssertEqual(context.viewState.footerMessage, message, "The error message should be shown.")
|
||||
XCTAssertTrue(context.viewState.isShowingFooterError, "There should be an error shown.")
|
||||
|
||||
// And when clearing the error.
|
||||
context.send(viewAction: .clearFooterError)
|
||||
|
||||
// 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 message should now be removed.
|
||||
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
|
||||
XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown again.")
|
||||
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error shown anymore.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,15 +62,17 @@ struct AuthenticationServerSelectionScreen: View {
|
|||
OnboardingIconImage(image: Asset.Images.authenticationServerSelectionIcon)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(VectorL10n.authenticationServerSelectionTitle)
|
||||
Text(viewModel.viewState.headerTitle)
|
||||
.font(theme.fonts.title2B)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("headerTitle")
|
||||
|
||||
Text(VectorL10n.authenticationServerSelectionMessage)
|
||||
Text(viewModel.viewState.headerMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.accessibilityIdentifier("headerMessage")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,11 +87,13 @@ struct AuthenticationServerSelectionScreen: View {
|
|||
textField
|
||||
}
|
||||
|
||||
Text(viewModel.viewState.footerMessage)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(textFieldFooterColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityIdentifier("textFieldFooter")
|
||||
if let errorMessage = viewModel.viewState.footerErrorMessage {
|
||||
Text(errorMessage)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(textFieldFooterColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityIdentifier("textFieldFooter")
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: submit) {
|
||||
|
|
|
@ -155,7 +155,8 @@ final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable {
|
|||
let modalRouter = NavigationRouter()
|
||||
|
||||
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
|
||||
loginWizard: loginWizard)
|
||||
loginWizard: loginWizard,
|
||||
homeserver: parameters.authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
|
|
|
@ -17,35 +17,10 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationSoftLogoutScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationSoftLogoutUITests(selector: #selector(verifyAuthenticationSoftLogoutScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationSoftLogoutScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationSoftLogoutScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredPassword:
|
||||
verifyEnteredPassword()
|
||||
case .ssoOnly:
|
||||
verifySSOOnly()
|
||||
case .noSSO:
|
||||
verifyNoSSO()
|
||||
case .fallback:
|
||||
verifyFallback()
|
||||
case .noKeyBackup:
|
||||
verifyNoKeyBackup()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
class AuthenticationSoftLogoutUITests: MockScreenTestCase {
|
||||
func testEmptyPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.emptyPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
|
@ -77,7 +52,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
|||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyEnteredPassword() {
|
||||
func testEnteredPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.enteredPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
|
@ -109,7 +86,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
|||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifySSOOnly() {
|
||||
func testSSOOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.ssoOnly.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
|
@ -138,7 +117,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
|||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoSSO() {
|
||||
func testNoSSO() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noSSO.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
|
@ -167,7 +148,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
|||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyFallback() {
|
||||
func testFallback() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.fallback.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
|
@ -197,7 +180,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
|||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoKeyBackup() {
|
||||
func testNoKeyBackup() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noKeyBackup.title)
|
||||
|
||||
XCTAssertFalse(app.staticTexts["messageLabel2"].exists, "The message 2 should not be shown.")
|
||||
}
|
||||
|
||||
|
|
|
@ -44,9 +44,15 @@ enum AuthenticationTermsViewModelResult {
|
|||
// MARK: View
|
||||
|
||||
struct AuthenticationTermsViewState: BindableState {
|
||||
/// The homeserver asking the user to accept the terms.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationTermsBindings
|
||||
|
||||
var headerMessage: String {
|
||||
VectorL10n.authenticationTermsMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether or not all of the policies have been accepted.
|
||||
var hasAcceptedAllPolicies: Bool {
|
||||
bindings.policies.allSatisfy(\.accepted)
|
||||
|
|
|
@ -33,8 +33,9 @@ class AuthenticationTermsViewModel: AuthenticationTermsViewModelType, Authentica
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(policies: [AuthenticationTermsPolicy]) {
|
||||
super.init(initialViewState: AuthenticationTermsViewState(bindings: AuthenticationTermsBindings(policies: policies)))
|
||||
init(homeserver: AuthenticationHomeserverViewData, policies: [AuthenticationTermsPolicy]) {
|
||||
super.init(initialViewState: AuthenticationTermsViewState(homeserver: homeserver,
|
||||
bindings: AuthenticationTermsBindings(policies: policies)))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
|
|
@ -22,8 +22,8 @@ struct AuthenticationTermsCoordinatorParameters {
|
|||
let registrationWizard: RegistrationWizard
|
||||
/// The policies to be accepted by the user.
|
||||
let localizedPolicies: [MXLoginPolicyData]
|
||||
/// The address of the homeserver (shown beneath the policies).
|
||||
let homeserverAddress: String
|
||||
/// The homeserver that provided the policies.
|
||||
let homeserver: AuthenticationState.Homeserver
|
||||
}
|
||||
|
||||
final class AuthenticationTermsCoordinator: Coordinator, Presentable {
|
||||
|
@ -59,10 +59,10 @@ final class AuthenticationTermsCoordinator: Coordinator, Presentable {
|
|||
@MainActor init(parameters: AuthenticationTermsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let subtitle = parameters.homeserverAddress
|
||||
let subtitle = parameters.homeserver.displayableAddress
|
||||
let policies = parameters.localizedPolicies.compactMap { AuthenticationTermsPolicy(url: $0.url, title: $0.name, subtitle: subtitle) }
|
||||
|
||||
let viewModel = AuthenticationTermsViewModel(policies: policies)
|
||||
let viewModel = AuthenticationTermsViewModel(homeserver: parameters.homeserver.viewData, policies: policies)
|
||||
let view = AuthenticationTermsScreen(viewModel: viewModel.context)
|
||||
authenticationTermsViewModel = viewModel
|
||||
authenticationTermsHostingController = VectorHostingController(rootView: view)
|
||||
|
|
|
@ -37,16 +37,18 @@ enum MockAuthenticationTermsScreenState: MockScreenState, CaseIterable {
|
|||
let viewModel: AuthenticationTermsViewModel
|
||||
switch self {
|
||||
case .matrixDotOrg:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
|
||||
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
subtitle: "matrix.org")])
|
||||
case .accepted:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
|
||||
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
subtitle: "matrix.org",
|
||||
accepted: true)])
|
||||
case .multiple:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockBasicServer, policies: [
|
||||
AuthenticationTermsPolicy(url: "https://example.com/terms", title: "Terms and Conditions", subtitle: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/privacy", title: "Privacy Policy", subtitle: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/conduct", title: "Code of Conduct", subtitle: "example.com")
|
||||
|
|
|
@ -17,26 +17,20 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationTermsUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationTermsScreenState.self
|
||||
class AuthenticationTermsUITests: MockScreenTestCase {
|
||||
func testMatrixDotOrg() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.matrixDotOrg.title)
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationTermsUITests(selector: #selector(verifyAuthenticationTermsScreen))
|
||||
|
||||
func testAccepted() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.accepted.title)
|
||||
verifyTerms(accepted: true)
|
||||
}
|
||||
|
||||
func verifyAuthenticationTermsScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationTermsScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
verifyTerms(accepted: false)
|
||||
case .accepted:
|
||||
verifyTerms(accepted: true)
|
||||
case .multiple:
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
func testMultiple() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.multiple.title)
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
func verifyTerms(accepted: Bool) {
|
||||
|
|
|
@ -62,7 +62,7 @@ struct AuthenticationTermsScreen: View {
|
|||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationTermsMessage)
|
||||
Text(viewModel.viewState.headerMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
|
|
@ -32,11 +32,18 @@ enum AuthenticationVerifyEmailViewModelResult {
|
|||
// MARK: View
|
||||
|
||||
struct AuthenticationVerifyEmailViewState: BindableState {
|
||||
/// The homeserver requesting email verification.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// An email has been sent and the app is waiting for the user to tap the link.
|
||||
var hasSentEmail = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationVerifyEmailBindings
|
||||
|
||||
/// The message shown in the header while asking for an email address to be entered.
|
||||
var formHeaderMessage: String {
|
||||
VectorL10n.authenticationVerifyEmailInputMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether the email address is valid and the user can continue.
|
||||
var hasInvalidAddress: Bool {
|
||||
bindings.emailAddress.isEmpty
|
||||
|
|
|
@ -31,8 +31,9 @@ class AuthenticationVerifyEmailViewModel: AuthenticationVerifyEmailViewModelType
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(emailAddress: String = "") {
|
||||
let viewState = AuthenticationVerifyEmailViewState(bindings: AuthenticationVerifyEmailBindings(emailAddress: emailAddress))
|
||||
init(homeserver: AuthenticationHomeserverViewData, emailAddress: String = "") {
|
||||
let viewState = AuthenticationVerifyEmailViewState(homeserver: homeserver,
|
||||
bindings: AuthenticationVerifyEmailBindings(emailAddress: emailAddress))
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ import CommonKit
|
|||
|
||||
struct AuthenticationVerifyEmailCoordinatorParameters {
|
||||
let registrationWizard: RegistrationWizard
|
||||
/// The homeserver that is requesting email verification.
|
||||
let homeserver: AuthenticationState.Homeserver
|
||||
}
|
||||
|
||||
final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
|
||||
|
@ -54,7 +56,7 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
|
|||
@MainActor init(parameters: AuthenticationVerifyEmailCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AuthenticationVerifyEmailViewModel()
|
||||
let viewModel = AuthenticationVerifyEmailViewModel(homeserver: parameters.homeserver.viewData)
|
||||
let view = AuthenticationVerifyEmailScreen(viewModel: viewModel.context)
|
||||
authenticationVerifyEmailViewModel = viewModel
|
||||
authenticationVerifyEmailHostingController = VectorHostingController(rootView: view)
|
||||
|
|
|
@ -37,11 +37,14 @@ enum MockAuthenticationVerifyEmailScreenState: MockScreenState, CaseIterable {
|
|||
let viewModel: AuthenticationVerifyEmailViewModel
|
||||
switch self {
|
||||
case .emptyAddress:
|
||||
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "")
|
||||
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "")
|
||||
case .enteredAddress:
|
||||
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "test@example.com")
|
||||
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "test@example.com")
|
||||
case .hasSentEmail:
|
||||
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "test@example.com")
|
||||
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
|
||||
emailAddress: "test@example.com")
|
||||
Task { await viewModel.updateForSentEmail() }
|
||||
}
|
||||
|
||||
|
|
|
@ -17,35 +17,17 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationVerifyEmailUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationVerifyEmailScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationVerifyEmailUITests(selector: #selector(verifyAuthenticationVerifyEmailScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationVerifyEmailScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationVerifyEmailScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyAddress:
|
||||
verifyEmptyAddress()
|
||||
case .enteredAddress:
|
||||
verifyEnteredAddress()
|
||||
case .hasSentEmail:
|
||||
verifyWaitingForEmailLink()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyAddress() {
|
||||
class AuthenticationVerifyEmailUITests: MockScreenTestCase {
|
||||
func testEmptyAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.emptyAddress.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
|
||||
|
||||
let addressTextField = app.textFields["addressTextField"]
|
||||
XCTAssertTrue(addressTextField.exists, "The text field should be shown before an email is sent.")
|
||||
XCTAssertEqual(addressTextField.value as? String, "Email Address", "The text field should be showing the placeholder before text is input.")
|
||||
XCTAssertEqual(addressTextField.value as? String, VectorL10n.authenticationVerifyEmailTextFieldPlaceholder,
|
||||
"The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let nextButton = app.buttons["nextButton"]
|
||||
XCTAssertTrue(nextButton.exists, "The next button should be shown before an email is sent.")
|
||||
|
@ -59,7 +41,9 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyEnteredAddress() {
|
||||
func testEnteredAddress() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.enteredAddress.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
|
||||
|
||||
|
@ -79,7 +63,9 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyWaitingForEmailLink() {
|
||||
func testWaitingForEmailLink() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.hasSentEmail.title)
|
||||
|
||||
XCTAssertFalse(app.staticTexts["titleLabel"].exists, "The title should be hidden once an email has been sent.")
|
||||
XCTAssertFalse(app.staticTexts["messageLabel"].exists, "The message should be hidden once an email has been sent.")
|
||||
XCTAssertFalse(app.textFields["addressTextField"].exists, "The text field should be hidden once an email has been sent.")
|
||||
|
|
|
@ -24,7 +24,7 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase {
|
|||
var context: AuthenticationVerifyEmailViewModelType.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = AuthenticationVerifyEmailViewModel()
|
||||
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ struct AuthenticationVerifyEmailForm: View {
|
|||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(VectorL10n.authenticationVerifyEmailInputMessage)
|
||||
Text(viewModel.viewState.formHeaderMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
|
|
@ -34,11 +34,18 @@ enum AuthenticationVerifyMsisdnViewModelResult {
|
|||
// MARK: View
|
||||
|
||||
struct AuthenticationVerifyMsisdnViewState: BindableState {
|
||||
/// The homeserver requesting MSISDN verification.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// An SMS has been sent.
|
||||
var hasSentSMS = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationVerifyMsisdnBindings
|
||||
|
||||
/// The message shown in the header while asking for a phone number to be entered.
|
||||
var formHeaderMessage: String {
|
||||
VectorL10n.authenticationVerifyMsisdnInputMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether the phone number is valid and the user can continue.
|
||||
var hasInvalidPhoneNumber: Bool {
|
||||
bindings.phoneNumber.isEmpty
|
||||
|
|
|
@ -31,8 +31,9 @@ class AuthenticationVerifyMsisdnViewModel: AuthenticationVerifyMsisdnViewModelTy
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(phoneNumber: String = "", otp: String = "") {
|
||||
let viewState = AuthenticationVerifyMsisdnViewState(bindings: AuthenticationVerifyMsisdnBindings(phoneNumber: phoneNumber, otp: otp))
|
||||
init(homeserver: AuthenticationHomeserverViewData, phoneNumber: String = "", otp: String = "") {
|
||||
let viewState = AuthenticationVerifyMsisdnViewState(homeserver: .mockMatrixDotOrg,
|
||||
bindings: AuthenticationVerifyMsisdnBindings(phoneNumber: phoneNumber, otp: otp))
|
||||
super.init(initialViewState: viewState)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ import libPhoneNumber_iOS
|
|||
|
||||
struct AuthenticationVerifyMsisdnCoordinatorParameters {
|
||||
let registrationWizard: RegistrationWizard
|
||||
/// The homeserver that is requesting MSISDN verification.
|
||||
let homeserver: AuthenticationState.Homeserver
|
||||
}
|
||||
|
||||
final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable {
|
||||
|
@ -55,7 +57,7 @@ final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable {
|
|||
@MainActor init(parameters: AuthenticationVerifyMsisdnCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = AuthenticationVerifyMsisdnViewModel()
|
||||
let viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: parameters.homeserver.viewData)
|
||||
let view = AuthenticationVerifyMsisdnScreen(viewModel: viewModel.context)
|
||||
authenticationVerifyMsisdnViewModel = viewModel
|
||||
authenticationVerifyMsisdnHostingController = VectorHostingController(rootView: view)
|
||||
|
|
|
@ -38,14 +38,19 @@ enum MockAuthenticationVerifyMsisdnScreenState: MockScreenState, CaseIterable {
|
|||
let viewModel: AuthenticationVerifyMsisdnViewModel
|
||||
switch self {
|
||||
case .emptyPhoneNumber:
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "")
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg,
|
||||
phoneNumber: "")
|
||||
case .enteredPhoneNumber:
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX")
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg,
|
||||
phoneNumber: "+44 XXXXXXXXX")
|
||||
case .hasSentSMS:
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX")
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg,
|
||||
phoneNumber: "+44 XXXXXXXXX")
|
||||
Task { await viewModel.updateForSentSMS() }
|
||||
case .enteredOTP:
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX", otp: "123456")
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg,
|
||||
phoneNumber: "+44 XXXXXXXXX",
|
||||
otp: "123456")
|
||||
Task { await viewModel.updateForSentSMS() }
|
||||
}
|
||||
|
||||
|
|
|
@ -17,31 +17,10 @@
|
|||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationVerifyMsisdnScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationVerifyMsisdnUITests(selector: #selector(verifyAuthenticationVerifyMsisdnScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationVerifyMsisdnScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationVerifyMsisdnScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPhoneNumber:
|
||||
verifyEmptyPhoneNumber()
|
||||
case .enteredPhoneNumber:
|
||||
verifyEnteredPhoneNumber()
|
||||
case .hasSentSMS:
|
||||
verifyHasSentSMS()
|
||||
case .enteredOTP:
|
||||
verifyEnteredOTP()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPhoneNumber() {
|
||||
class AuthenticationVerifyMsisdnUITests: MockScreenTestCase {
|
||||
func testEmptyPhoneNumber() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.emptyPhoneNumber.title)
|
||||
|
||||
let titleLabel = app.staticTexts["titleLabel"]
|
||||
XCTAssertTrue(titleLabel.exists, "The title should be shown.")
|
||||
|
||||
|
@ -50,7 +29,8 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
|||
|
||||
let phoneNumberTextField = app.textFields["phoneNumberTextField"]
|
||||
XCTAssertTrue(phoneNumberTextField.exists, "The text field should be shown before an SMS is sent.")
|
||||
XCTAssertEqual(phoneNumberTextField.value as? String, "Phone Number", "The text field should be showing the placeholder before text is input.")
|
||||
XCTAssertEqual(phoneNumberTextField.value as? String, VectorL10n.authenticationVerifyMsisdnTextFieldPlaceholder,
|
||||
"The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let nextButton = app.buttons["nextButton"]
|
||||
XCTAssertTrue(nextButton.exists, "The next button should be shown.")
|
||||
|
@ -64,7 +44,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyEnteredPhoneNumber() {
|
||||
func testEnteredPhoneNumber() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.enteredPhoneNumber.title)
|
||||
|
||||
let titleLabel = app.staticTexts["titleLabel"]
|
||||
XCTAssertTrue(titleLabel.exists, "The title should be shown.")
|
||||
|
||||
|
@ -87,7 +69,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
|||
XCTAssertEqual(cancelButton.label, "Cancel")
|
||||
}
|
||||
|
||||
func verifyHasSentSMS() {
|
||||
func testHasSentSMS() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.hasSentSMS.title)
|
||||
|
||||
let titleLabel = app.staticTexts["titleLabel"]
|
||||
XCTAssertTrue(titleLabel.exists, "The title should be shown.")
|
||||
|
||||
|
@ -99,7 +83,8 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
|||
|
||||
let otpTextField = app.textFields["otpTextField"]
|
||||
XCTAssertTrue(otpTextField.exists, "The OTP text field should be shown once an SMS has been sent.")
|
||||
XCTAssertEqual(otpTextField.value as? String, "Verification Code", "The text field should be showing the placeholder before text is input.")
|
||||
XCTAssertEqual(otpTextField.value as? String, VectorL10n.authenticationVerifyMsisdnOtpTextFieldPlaceholder,
|
||||
"The text field should be showing the placeholder before text is input.")
|
||||
|
||||
let nextButton = app.buttons["nextButton"]
|
||||
XCTAssertTrue(nextButton.exists, "The next button should be shown.")
|
||||
|
@ -114,7 +99,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest {
|
|||
XCTAssertEqual(backButton.label, "Back")
|
||||
}
|
||||
|
||||
func verifyEnteredOTP() {
|
||||
func testEnteredOTP() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.enteredOTP.title)
|
||||
|
||||
let titleLabel = app.staticTexts["titleLabel"]
|
||||
XCTAssertTrue(titleLabel.exists, "The title should be shown.")
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class AuthenticationVerifyMsisdnViewModelTests: XCTestCase {
|
|||
var context: AuthenticationVerifyMsisdnViewModelType.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel()
|
||||
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ struct AuthenticationVerifyMsisdnForm: View {
|
|||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibilityIdentifier("titleLabel")
|
||||
|
||||
Text(VectorL10n.authenticationVerifyMsisdnInputMessage)
|
||||
Text(viewModel.viewState.formHeaderMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
|
|
@ -18,34 +18,59 @@ import SwiftUI
|
|||
|
||||
struct ScreenList: View {
|
||||
|
||||
private var allStates: [ScreenStateInfo]
|
||||
private let allStates: [ScreenStateInfo]
|
||||
|
||||
@State private var searchQuery = ""
|
||||
@State private var filteredStates: [ScreenStateInfo]
|
||||
|
||||
init(screens: [MockScreenState.Type]) {
|
||||
allStates = screens
|
||||
.map({ $0.stateRenderer })
|
||||
.flatMap{( $0.states )}
|
||||
let states = screens
|
||||
.map { $0.stateRenderer }
|
||||
.flatMap { $0.states }
|
||||
|
||||
allStates = states
|
||||
filteredStates = states
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
SwiftUI.Section {
|
||||
ForEach(0..<allStates.count, id: \.self) { i in
|
||||
let state = allStates[i]
|
||||
NavigationLink(destination: state.view) {
|
||||
Text(state.screenTitle)
|
||||
VStack {
|
||||
TextField("Search", text: $searchQuery)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal)
|
||||
.accessibilityIdentifier("searchQueryTextField")
|
||||
.onChange(of: searchQuery, perform: search)
|
||||
|
||||
Form {
|
||||
SwiftUI.Section {
|
||||
ForEach(0..<filteredStates.count, id: \.self) { i in
|
||||
let state = filteredStates[i]
|
||||
NavigationLink(destination: state.view) {
|
||||
Text(state.screenTitle)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text("End of list")
|
||||
.accessibilityIdentifier("footerText")
|
||||
}
|
||||
}
|
||||
|
||||
SwiftUI.Section {
|
||||
Text("Last Item")
|
||||
.accessibilityIdentifier("lastItem")
|
||||
}
|
||||
}
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle("SwiftUI Screens")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationTitle("Screen States")
|
||||
}
|
||||
|
||||
func search(query: String) {
|
||||
if query.isEmpty {
|
||||
filteredStates = allStates
|
||||
} else {
|
||||
filteredStates = allStates.filter {
|
||||
$0.screenTitle.localizedStandardContains(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScreenList_Previews: PreviewProvider {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue