Merge branch 'develop' of github.com:vector-im/element-ios into langleyd/6419_remove_ffmpeg_again

This commit is contained in:
David Langley 2022-07-21 17:31:08 +01:00
commit 7bec008538
147 changed files with 1414 additions and 1454 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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' } }

View file

@ -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

View file

@ -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" = "Well need some info to get you set up.";
"authentication_registration_username" = "Username";
"authentication_registration_username_footer" = "You cant 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 its 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 its 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.";

View file

@ -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. Its trusted by the worlds most secure organisations.";
"onboarding_use_case_title" = "Who will you chat to the most?";
"onboarding_use_case_message" = "Well help you get connected.";
"onboarding_use_case_message" = "Well 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" = "Youre 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 cant 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 its 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

View file

@ -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 its 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 cant 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")
}
/// Youre 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")
}
/// Well help you get connected.
/// Well 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")
}

View file

@ -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 its 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")
}
/// Well 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 cant 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 its 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

View file

@ -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;

View file

@ -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)
{

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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;

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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)
{

View file

@ -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

View file

@ -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:

View file

@ -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 {

View file

@ -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];
}

View file

@ -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)
}
}

View file

@ -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.")

View file

@ -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: [])

View file

@ -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()

View file

@ -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.

View file

@ -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 ?? [])

View file

@ -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.

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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() }
}

View file

@ -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.")

View file

@ -24,7 +24,7 @@ class AuthenticationForgotPasswordViewModelTests: XCTestCase {
var context: AuthenticationForgotPasswordViewModelType.Context!
override func setUpWithError() throws {
viewModel = AuthenticationForgotPasswordViewModel()
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg)
context = viewModel.context
}

View file

@ -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)

View file

@ -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 }

View file

@ -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.

View file

@ -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() {

View file

@ -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,

View file

@ -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.
}

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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.

View file

@ -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
}
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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).")
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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.

View file

@ -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))
}

View file

@ -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

View file

@ -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)
}

View file

@ -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.")

View file

@ -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.")
}
}

View file

@ -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) {

View file

@ -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 }

View file

@ -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.")
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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) {

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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() }
}

View file

@ -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.")

View file

@ -24,7 +24,7 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase {
var context: AuthenticationVerifyEmailViewModelType.Context!
override func setUpWithError() throws {
viewModel = AuthenticationVerifyEmailViewModel()
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg)
context = viewModel.context
}

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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() }
}

View file

@ -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.")

View file

@ -24,7 +24,7 @@ class AuthenticationVerifyMsisdnViewModelTests: XCTestCase {
var context: AuthenticationVerifyMsisdnViewModelType.Context!
override func setUpWithError() throws {
viewModel = AuthenticationVerifyMsisdnViewModel()
viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg)
context = viewModel.context
}

View file

@ -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)

View file

@ -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