mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-07 00:17:43 +02:00
Merge pull request #6480 from vector-im/release/1.8.24/release
Release 1.8.24
This commit is contained in:
@@ -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
|
||||
|
||||
+38
@@ -1,3 +1,41 @@
|
||||
## Changes in 1.8.24 (2022-07-26)
|
||||
|
||||
✨ Features
|
||||
|
||||
- Enable the new authentication and personalisation flows in the onboarding coordinator. ([#5151](https://github.com/vector-im/element-ios/issues/5151))
|
||||
- Read tile server URL from .well-known (PSG-592) ([#6472](https://github.com/vector-im/element-ios/issues/6472))
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Upgrade MatrixSDK version ([v0.23.13](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.13)).
|
||||
- Replaces the usage of ffmpeg in the app again(Change was previously reverted). ([#6419](https://github.com/vector-im/element-ios/issues/6419))
|
||||
- Location sharing: Handle live location sharing start event reply in the timeline. ([#6423](https://github.com/vector-im/element-ios/issues/6423))
|
||||
- Location sharing: Show map credits on live location timeline tile only when map is shown. ([#6448](https://github.com/vector-im/element-ios/issues/6448))
|
||||
- Location sharing: Handle live location sharing delete in the timeline. ([#6470](https://github.com/vector-im/element-ios/issues/6470))
|
||||
- Location sharing: Display clearer error message when the user doesn't have permission to share location in the room. ([#6477](https://github.com/vector-im/element-ios/issues/6477))
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- Registration: Trim any whitespace away when verifying the user's email address. ([#2594](https://github.com/vector-im/element-ios/issues/2594))
|
||||
- AuthenticationViewController is now correctly configured for a deep link if the link is opened before the view gets shown. ([#6425](https://github.com/vector-im/element-ios/issues/6425))
|
||||
|
||||
🧱 Build
|
||||
|
||||
- Fix UI tests failing on CI but not being reported by prefixing all tests with `test`. ([#6432](https://github.com/vector-im/element-ios/issues/6432))
|
||||
|
||||
🚧 In development 🚧
|
||||
|
||||
- Update strings for FTUE authentication flow following final review. ([#6427](https://github.com/vector-im/element-ios/issues/6427))
|
||||
- Check for a phone number during login and send an MSISDN when using the new flow. ([#6428](https://github.com/vector-im/element-ios/issues/6428))
|
||||
- Fix ReCaptcha form sometimes being slow to react to taps in the new flow. ([#6429](https://github.com/vector-im/element-ios/issues/6429))
|
||||
- When entering a full MXID during registration on the new flow, update the homeserver to match. ([#6430](https://github.com/vector-im/element-ios/issues/6430))
|
||||
- Always perform the dummy stage in the registration wizard, irregardless of whether it is mandatory or optional. ([#6459](https://github.com/vector-im/element-ios/issues/6459))
|
||||
|
||||
Others
|
||||
|
||||
- Crypto: Convert verification request and transaction to protocols ([#6444](https://github.com/vector-im/element-ios/pull/6444))
|
||||
|
||||
|
||||
## Changes in 1.8.23 (2022-07-15)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.8.23
|
||||
CURRENT_PROJECT_VERSION = 1.8.23
|
||||
MARKETING_VERSION = 1.8.24
|
||||
CURRENT_PROJECT_VERSION = 1.8.24
|
||||
|
||||
@@ -384,8 +384,8 @@ final class BuildSettings: NSObject {
|
||||
static let authEnableRefreshTokens = false
|
||||
|
||||
// MARK: - Onboarding
|
||||
static let onboardingShowAccountPersonalization = false
|
||||
static let onboardingEnableNewAuthenticationFlow = false
|
||||
static let onboardingShowAccountPersonalization = true
|
||||
static let onboardingEnableNewAuthenticationFlow = true
|
||||
|
||||
// MARK: - Unified Search
|
||||
static let unifiedSearchScreenShowPublicDirectory = true
|
||||
@@ -402,7 +402,8 @@ final class BuildSettings: NSObject {
|
||||
|
||||
// MARK: - Location Sharing
|
||||
|
||||
static let tileServerMapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
|
||||
/// Overwritten by the home server's .well-known configuration (if any exists)
|
||||
static let defaultTileServerMapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
|
||||
|
||||
static let locationSharingEnabled = true
|
||||
|
||||
|
||||
@@ -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.12'
|
||||
$matrixSDKVersion = '= 0.23.13'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
||||
@@ -27,6 +27,24 @@
|
||||
"version" : "5.12.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "ogg-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vector-im/ogg-swift.git",
|
||||
"state" : {
|
||||
"revision" : "9d82ed838404f10b607a1a1689f404563e9115c3",
|
||||
"version" : "0.8.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "opus-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vector-im/opus-swift",
|
||||
"state" : {
|
||||
"revision" : "11f1887767cbc87c4b64b789ee830b779cc744cb",
|
||||
"version" : "0.8.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -36,6 +54,15 @@
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-ogg",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vector-im/swift-ogg",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "0ffad3f7b45a6a4760db090d503b00f094bbecc0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -19,72 +19,3 @@
|
||||
// MARK: Onboarding Personalization WIP
|
||||
|
||||
"image_picker_action_files" = "Choose from files";
|
||||
|
||||
// MARK: Onboarding Authentication WIP
|
||||
"authentication_registration_title" = "Create your account";
|
||||
"authentication_registration_message" = "We’ll need some info to get you set up.";
|
||||
"authentication_registration_username" = "Username";
|
||||
"authentication_registration_username_footer" = "You can’t change this later";
|
||||
"authentication_registration_password_footer" = "Must be 8 characters or more";
|
||||
|
||||
"authentication_login_title" = "Welcome back!";
|
||||
"authentication_login_username" = "Username or Email";
|
||||
"authentication_login_forgot_password" = "Forgot password";
|
||||
|
||||
"authentication_server_info_title" = "Choose your server to store your data";
|
||||
"authentication_server_info_matrix_description" = "Join millions for free on the largest public server";
|
||||
|
||||
"authentication_server_selection_title" = "Choose your server";
|
||||
"authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data.";
|
||||
"authentication_server_selection_server_url" = "Server URL";
|
||||
"authentication_server_selection_server_footer" = "You can only connect to a server that has already been set up";
|
||||
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
|
||||
|
||||
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
|
||||
|
||||
"authentication_verify_email_input_title" = "Enter your email address";
|
||||
"authentication_verify_email_input_message" = "This will help verify your account and enables password recovery.";
|
||||
"authentication_verify_email_text_field_placeholder" = "Email Address";
|
||||
"authentication_verify_email_waiting_title" = "Check your email to verify.";
|
||||
"authentication_verify_email_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
|
||||
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_verify_email_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_forgot_password_input_title" = "Enter your email address";
|
||||
"authentication_forgot_password_input_message" = "We will send you a verification link.";
|
||||
"authentication_forgot_password_text_field_placeholder" = "Email Address";
|
||||
"authentication_forgot_password_waiting_title" = "Check your email";
|
||||
"authentication_forgot_password_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
|
||||
"authentication_forgot_password_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_forgot_password_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_choose_password_input_title" = "Choose a new password";
|
||||
"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more.";
|
||||
"authentication_choose_password_text_field_placeholder" = "New Password";
|
||||
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
|
||||
"authentication_choose_password_submit_button" = "Reset Password";
|
||||
|
||||
"authentication_verify_msisdn_input_title" = "Enter your phone number";
|
||||
"authentication_verify_msisdn_input_message" = "This will help verify your account and enables password recovery.";
|
||||
"authentication_verify_msisdn_text_field_placeholder" = "Phone Number";
|
||||
"authentication_verify_msisdn_otp_text_field_placeholder" = "Verification Code";
|
||||
"authentication_verify_msisdn_waiting_title" = "Confirm your phone number";
|
||||
"authentication_verify_msisdn_waiting_message" = "We just sent a code to %@. Enter it below to verify it’s you.";
|
||||
"authentication_verify_msisdn_waiting_button" = "Resend code";
|
||||
"authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number";
|
||||
|
||||
"authentication_terms_title" = "Privacy policy";
|
||||
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
|
||||
"authentication_terms_policy_url_error" = "Unable to find the selected policy. Please try again later.";
|
||||
|
||||
"authentication_recaptcha_message" = "This server would like to make sure you are not a robot";
|
||||
|
||||
// MARK: Password Validation
|
||||
"password_validation_info_header" = "Your password should meet the criteria below:";
|
||||
"password_validation_error_header" = "Given password does not meet the criteria below:";
|
||||
"password_validation_error_min_length" = "At least %d characters.";
|
||||
"password_validation_error_max_length" = "Not exceed %d characters.";
|
||||
"password_validation_error_contain_lowercase_letter" = "Contain a lower-case letter.";
|
||||
"password_validation_error_contain_uppercase_letter" = "Contain an upper-case letter.";
|
||||
"password_validation_error_contain_number" = "Contain a number.";
|
||||
"password_validation_error_contain_symbol" = "Contain a symbol.";
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"accessibility_checkbox_label" = "checkbox";
|
||||
"accessibility_button_label" = "button";
|
||||
|
||||
// Onboarding
|
||||
// MARK: Onboarding
|
||||
"onboarding_splash_register_button_title" = "Create account";
|
||||
"onboarding_splash_login_button_title" = "I already have an account";
|
||||
"onboarding_splash_page_1_title" = "Own your conversations.";
|
||||
@@ -109,19 +109,19 @@
|
||||
"onboarding_splash_page_4_message" = "Element is also great for the workplace. It’s trusted by the world’s most secure organisations.";
|
||||
|
||||
"onboarding_use_case_title" = "Who will you chat to the most?";
|
||||
"onboarding_use_case_message" = "We’ll help you get connected.";
|
||||
"onboarding_use_case_message" = "We’ll help you get connected";
|
||||
"onboarding_use_case_personal_messaging" = "Friends and family";
|
||||
"onboarding_use_case_work_messaging" = "Teams";
|
||||
"onboarding_use_case_community_messaging" = "Communities";
|
||||
/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */
|
||||
"onboarding_use_case_not_sure_yet" = "Not sure yet? You can %@";
|
||||
"onboarding_use_case_skip_button" = "skip this question";
|
||||
"onboarding_use_case_not_sure_yet" = "Not sure yet? %@";
|
||||
"onboarding_use_case_skip_button" = "Skip this question";
|
||||
"onboarding_use_case_existing_server_message" = "Looking to join an existing server?";
|
||||
"onboarding_use_case_existing_server_button" = "Connect to server";
|
||||
|
||||
"onboarding_congratulations_title" = "Congratulations!";
|
||||
/* The placeholder string contains the user's matrix ID */
|
||||
"onboarding_congratulations_message" = "Your account %@ has been created.";
|
||||
"onboarding_congratulations_message" = "Your account %@ has been created";
|
||||
"onboarding_congratulations_personalize_button" = "Personalise profile";
|
||||
"onboarding_congratulations_home_button" = "Take me home";
|
||||
|
||||
@@ -135,14 +135,95 @@
|
||||
"onboarding_display_name_max_length" = "Your display name must be less than 256 characters";
|
||||
|
||||
"onboarding_avatar_title" = "Add a profile picture";
|
||||
"onboarding_avatar_message" = "You can change this anytime.";
|
||||
"onboarding_avatar_message" = "Time to put a face to the name";
|
||||
"onboarding_avatar_accessibility_label" = "Profile picture";
|
||||
|
||||
"onboarding_celebration_title" = "You’re all set!";
|
||||
"onboarding_celebration_message" = "Your preferences have been saved.";
|
||||
"onboarding_celebration_title" = "Looking good!";
|
||||
"onboarding_celebration_message" = "Head to settings anytime to update your profile";
|
||||
"onboarding_celebration_button" = "Let's go";
|
||||
|
||||
// Authentication
|
||||
// MARK: Authentication
|
||||
"authentication_registration_title" = "Create your account";
|
||||
"authentication_registration_username" = "Username";
|
||||
"authentication_registration_username_footer" = "You can’t change this later";
|
||||
/* The placeholder will show the full Matrix ID that has been entered. */
|
||||
"authentication_registration_username_footer_available" = "Others can discover you %@";
|
||||
"authentication_registration_password_footer" = "Must be 8 characters or more";
|
||||
"authentication_server_info_title" = "Where your conversations will live";
|
||||
|
||||
"authentication_login_title" = "Welcome back!";
|
||||
"authentication_login_username" = "Username / Email / Phone";
|
||||
"authentication_login_forgot_password" = "Forgot password";
|
||||
"authentication_server_info_title_login" = "Where your conversations live";
|
||||
|
||||
"authentication_server_selection_login_title" = "Connect to homeserver";
|
||||
"authentication_server_selection_login_message" = "What is the address of your server?";
|
||||
"authentication_server_selection_register_title" = "Select your homeserver";
|
||||
"authentication_server_selection_register_message" = "What is the address of your server? This is like a home for all your data";
|
||||
"authentication_server_selection_server_url" = "Homeserver URL";
|
||||
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
|
||||
|
||||
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
|
||||
|
||||
"authentication_verify_email_input_title" = "Enter your email";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_verify_email_input_message" = "%@ needs to verify your account";
|
||||
"authentication_verify_email_text_field_placeholder" = "Email";
|
||||
"authentication_verify_email_waiting_title" = "Verify your email.";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_verify_email_waiting_message" = "Follow the instructions sent to %@";
|
||||
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
|
||||
"authentication_verify_email_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_forgot_password_input_title" = "Enter your email";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_forgot_password_input_message" = "%@ will send you a verification link";
|
||||
"authentication_forgot_password_text_field_placeholder" = "Email";
|
||||
"authentication_forgot_password_waiting_title" = "Check your email.";
|
||||
/* The placeholder will show the email address that was entered. */
|
||||
"authentication_forgot_password_waiting_message" = "Follow the instructions sent to %@";
|
||||
"authentication_forgot_password_waiting_button" = "Resend email";
|
||||
|
||||
"authentication_choose_password_input_title" = "Choose a new password";
|
||||
"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more";
|
||||
"authentication_choose_password_text_field_placeholder" = "New Password";
|
||||
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
|
||||
"authentication_choose_password_submit_button" = "Reset Password";
|
||||
"authentication_choose_password_not_verified_title" = "Email not verified";
|
||||
"authentication_choose_password_not_verified_message" = "Check your inbox";
|
||||
|
||||
"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";
|
||||
@@ -2152,6 +2233,9 @@ Tap the + to start adding people.";
|
||||
|
||||
"location_sharing_invalid_authorization_settings" = "Settings";
|
||||
|
||||
"location_sharing_invalid_power_level_title" = "You don’t have permission to share live location";
|
||||
"location_sharing_invalid_power_level_message" = "You need to have the right permissions in order to share live location in this room.";
|
||||
|
||||
"location_sharing_open_apple_maps" = "Open in Apple Maps";
|
||||
|
||||
"location_sharing_open_google_maps" = "Open in Google Maps";
|
||||
@@ -2445,6 +2529,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
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This application is making use of the following third party softwares:
|
||||
This application is making use of the following third party softwares:
|
||||
</p>
|
||||
<ul>
|
||||
<ul>
|
||||
<li>
|
||||
<b>MatrixSDK</b> (<a
|
||||
href="https://github.com/matrix-org/matrix-ios-sdk.git">https://github.com/matrix-org/matrix-ios-sdk.git</a>)
|
||||
@@ -42,38 +42,38 @@
|
||||
<br/><br/>Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<b>AFNetworking</b> (<a
|
||||
href="https://github.com/AFNetworking/AFNetworking">https://github.com/AFNetworking/AFNetworking</a>)
|
||||
<br/><br/>AFNetworking is a networking library for iOS and Mac OS X. It's built on top of the Foundation URL Loading System, extending the powerful high-level networking abstractions built into Cocoa.
|
||||
<br/><br/>It is released under the MIT license.
|
||||
<br/><br/>AFNetworking is a networking library for iOS and Mac OS X. It's built on top of the Foundation URL Loading System, extending the powerful high-level networking abstractions built into Cocoa.
|
||||
<br/><br/>It is released under the MIT license.
|
||||
<br/>Copyright (c) 2011-2016 Alamofire Software Foundation (<a
|
||||
href="https://alamofire.org/">https://alamofire.org/</a>)
|
||||
<br/><br/>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<br/><br/>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b>HPGrowingTextView</b> (<a
|
||||
href="https://github.com/HansPinckaers/GrowingTextView">https://github.com/HansPinckaers/GrowingTextView</a>)
|
||||
<br/><br/>Multi-line/Autoresizing UITextView similar as in the SMS-app.
|
||||
<br/><br/>It is released under the MIT license.
|
||||
<br/>Copyright (c) 2011 Hans Pinckaers
|
||||
<br/><br/>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<br/><br/>Multi-line/Autoresizing UITextView similar as in the SMS-app.
|
||||
<br/><br/>It is released under the MIT license.
|
||||
<br/>Copyright (c) 2011 Hans Pinckaers
|
||||
<br/><br/>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b>libPhoneNumber-iOS</b> (<a
|
||||
href="https://github.com/iziz/libPhoneNumber-iOS.git">https://github.com/iziz/libPhoneNumber-iOS.git</a>)
|
||||
<br/><br/>iOS library for parsing, formatting, storing and validating international phone numbers from libphonenumber library.
|
||||
<br/><br/>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at:
|
||||
<br/><br/>iOS library for parsing, formatting, storing and validating international phone numbers from libphonenumber library.
|
||||
<br/><br/>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at:
|
||||
<br/><br/><a
|
||||
href="https://www.apache.org/licenses/LICENSE-2.0">https://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
<br/><br/>Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
<br/><br/>
|
||||
<br/><br/>Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b>cmark</b> (<a
|
||||
@@ -124,7 +124,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<b>Jitsi Meet iOS SDK</b> (<a
|
||||
href=" https://github.com/jitsi/jitsi-meet-ios-sdk-releases">Jitsi Meet iOS SDK binaries</a>)
|
||||
href="https://github.com/jitsi/jitsi-meet-ios-sdk-releases">Jitsi Meet iOS SDK binaries</a>)
|
||||
<p>It is composed of 2 frameworks:<br/></p>
|
||||
<ul>
|
||||
<li>
|
||||
@@ -1362,6 +1362,33 @@
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
<b>DGCollectionViewLeftAlignFlowLayout</b> (<a href="https://github.com/Digipolitan/collection-view-left-align-flow-layout">https://github.com/Digipolitan/collection-view-left-align-flow-layout</a>)
|
||||
<br/><br/>This is a simple layout that align does not try to fulfill the lines but stick elements to the left.
|
||||
<br/><br/>DGCollectionViewLeftAlignFlowLayout is licensed under the BSD 3-Clause license.
|
||||
<br/>Copyright (c) 2017, Digipolitan All rights reserved.
|
||||
<br/><br/>Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
<br/><br/>- Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
<br/><br/>- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
<br/><br/>- Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
<br/><br/>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b>KTCenterFlowLayout</b> (<a href="https://github.com/keighl/KTCenterFlowLayout">https://github.com/keighl/KTCenterFlowLayout</a>)
|
||||
<br/><br/>KTCenterFlowLayout is a subclass of UICollectionViewFlowLayout which Aligns cells to the center of a collection view.
|
||||
@@ -1673,176 +1700,109 @@
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<b>ffmpeg-kit-ios-audio</b> (<a href="https://github.com/tanersener/ffmpeg-kit">https://github.com/tanersener/ffmpeg-kit</a>)
|
||||
<br/><br/>
|
||||
<pre>
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
<b>swift-ogg</b> (<a
|
||||
href="https://github.com/vector-im/swift-ogg">https://github.com/vector-im/swift-ogg</a>)
|
||||
<p>Makes use of code from 5 frameworks:<br/></p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>It contains code adapted from:<br/></p>
|
||||
watson-developer-cloud/swift-sdk (<a
|
||||
href="https://github.com/watson-developer-cloud/swift-sdk">https://github.com/watson-developer-cloud/swift-sdk</a>)
|
||||
<br/><br/>Copyright 2018-present 8x8, Inc.
|
||||
<br/><br/>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:
|
||||
<br/><br/><a
|
||||
href="https://www.apache.org/licenses/LICENSE-2.0">https://www.apache.org/licenses/LICENSE-2.0</a>
|
||||
<br/><br/>Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
opus-swift (<a
|
||||
href="https://github.com/ybrid/opus-swift">https://github.com/ybrid/opus-swift</a>)
|
||||
<br/><br/>
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
Copyright (c) 2021 nacamar GmbH - Ybrid®, a Hybrid Dynamic Live Audio Technology
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
opus (<a
|
||||
href="https://opus-codec.org/downloads/">https://opus-codec.org/downloads/</a>)
|
||||
<br/><br/>
|
||||
Both the reference implementation and the revised implementations on opus-codec.org are available under the three-clause BSD license. This BSD license is compatible with all common open source and commercial software licenses, see [details](https://opus-codec.org/license).
|
||||
|
||||
0. Additional Definitions.
|
||||
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry,
|
||||
CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
Opus is subject to the royalty-free patent licenses which are specified at:
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
Xiph.Org Foundation:
|
||||
https://datatracker.ietf.org/ipr/1524/
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
Microsoft Corporation:
|
||||
https://datatracker.ietf.org/ipr/1914/
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
Broadcom Corporation:
|
||||
https://datatracker.ietf.org/ipr/1526/
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
ogg-swift (<a
|
||||
href="https://github.com/ybrid/ogg-swift">https://github.com/ybrid/ogg-swift</a>)
|
||||
<br/><br/>
|
||||
MIT License
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
Copyright (c) 2021 Ybrid®, a Hybrid Dynamic Live Audio Technology
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
ogg (<a
|
||||
href="https://xiph.org/downloads/">https://xiph.org/downloads/</a>)
|
||||
<br/><br/>
|
||||
Ogg is licensed under the [New BSD License](https://wiki.xiph.org/XiphWiki:Copyrights).
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
Copyright (c) 2002, Xiph.org Foundation
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
The New BSD License states that:
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
</pre>
|
||||
Redistribution and use in source and binary forms [of this work], with or without modification, are permitted provided that the following conditions are met:
|
||||
Redistributions of source code must retain the copyright notice, this list of conditions, and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the copyright notice, this list of conditions, and the following disclaimer in the documentation, and/or other materials provided with the distribution.
|
||||
Neither the name of the Xiph.Org Foundation nor the names of its contributors may be used to endorse or promote products derived from this work without specific prior written permission.
|
||||
THIS WORK IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS WORK, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
<br/><br/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>WeakDictionary</b> (<a href="https://github.com/nicholascross/WeakDictionary">https://github.com/nicholascross/WeakDictionary/</a>)
|
||||
@@ -1900,8 +1860,6 @@ Library.
|
||||
</li>
|
||||
<li>
|
||||
<b>UICollectionViewRightAlignedLayout</b> (<a href="https://github.com/mokagio/UICollectionViewRightAlignedLayout">https://github.com/mokagio/UICollectionViewRightAlignedLayout</a>)
|
||||
<br/>
|
||||
<b>UICollectionViewLeftAlignedLayout</b> (<a href="https://github.com/mokagio/UICollectionViewLeftAlignedLayout">https://github.com/mokagio/UICollectionViewLeftAlignedLayout</a>)
|
||||
<br/><br/>
|
||||
The MIT License (MIT)
|
||||
<br/><br/>
|
||||
|
||||
@@ -531,6 +531,202 @@ public class VectorL10n: NSObject {
|
||||
public static var authenticatedSessionFlowNotSupported: String {
|
||||
return VectorL10n.tr("Vector", "authenticated_session_flow_not_supported")
|
||||
}
|
||||
/// Your account is not created yet. Stop the registration process?
|
||||
public static var authenticationCancelFlowConfirmationMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_cancel_flow_confirmation_message")
|
||||
}
|
||||
/// Make sure it’s 8 characters or more
|
||||
public static var authenticationChoosePasswordInputMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_input_message")
|
||||
}
|
||||
/// Choose a new password
|
||||
public static var authenticationChoosePasswordInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_input_title")
|
||||
}
|
||||
/// Check your inbox
|
||||
public static var authenticationChoosePasswordNotVerifiedMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_not_verified_message")
|
||||
}
|
||||
/// Email not verified
|
||||
public static var authenticationChoosePasswordNotVerifiedTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_not_verified_title")
|
||||
}
|
||||
/// Sign out of all devices
|
||||
public static var authenticationChoosePasswordSignoutAllDevices: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_signout_all_devices")
|
||||
}
|
||||
/// Reset Password
|
||||
public static var authenticationChoosePasswordSubmitButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_submit_button")
|
||||
}
|
||||
/// New Password
|
||||
public static var authenticationChoosePasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_choose_password_text_field_placeholder")
|
||||
}
|
||||
/// %@ will send you a verification link
|
||||
public static func authenticationForgotPasswordInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_input_message", p1)
|
||||
}
|
||||
/// Enter your email
|
||||
public static var authenticationForgotPasswordInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_input_title")
|
||||
}
|
||||
/// Email
|
||||
public static var authenticationForgotPasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
public static var authenticationForgotPasswordWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_button")
|
||||
}
|
||||
/// Follow the instructions sent to %@
|
||||
public static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_message", p1)
|
||||
}
|
||||
/// Check your email.
|
||||
public static var authenticationForgotPasswordWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_title")
|
||||
}
|
||||
/// Forgot password
|
||||
public static var authenticationLoginForgotPassword: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_forgot_password")
|
||||
}
|
||||
/// Welcome back!
|
||||
public static var authenticationLoginTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_title")
|
||||
}
|
||||
/// Username / Email / Phone
|
||||
public static var authenticationLoginUsername: String {
|
||||
return VectorL10n.tr("Vector", "authentication_login_username")
|
||||
}
|
||||
/// Are you a human?
|
||||
public static var authenticationRecaptchaTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_recaptcha_title")
|
||||
}
|
||||
/// Must be 8 characters or more
|
||||
public static var authenticationRegistrationPasswordFooter: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_password_footer")
|
||||
}
|
||||
/// Create your account
|
||||
public static var authenticationRegistrationTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_title")
|
||||
}
|
||||
/// Username
|
||||
public static var authenticationRegistrationUsername: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username")
|
||||
}
|
||||
/// You can’t change this later
|
||||
public static var authenticationRegistrationUsernameFooter: String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username_footer")
|
||||
}
|
||||
/// Others can discover you %@
|
||||
public static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_registration_username_footer_available", p1)
|
||||
}
|
||||
/// Where your conversations will live
|
||||
public static var authenticationServerInfoTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_info_title")
|
||||
}
|
||||
/// Where your conversations live
|
||||
public static var authenticationServerInfoTitleLogin: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_info_title_login")
|
||||
}
|
||||
/// Cannot find a server at this URL, please check it is correct.
|
||||
public static var authenticationServerSelectionGenericError: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_generic_error")
|
||||
}
|
||||
/// What is the address of your server?
|
||||
public static var authenticationServerSelectionLoginMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_login_message")
|
||||
}
|
||||
/// Connect to homeserver
|
||||
public static var authenticationServerSelectionLoginTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_login_title")
|
||||
}
|
||||
/// What is the address of your server? This is like a home for all your data
|
||||
public static var authenticationServerSelectionRegisterMessage: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_register_message")
|
||||
}
|
||||
/// Select your homeserver
|
||||
public static var authenticationServerSelectionRegisterTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_register_title")
|
||||
}
|
||||
/// Homeserver URL
|
||||
public static var authenticationServerSelectionServerUrl: String {
|
||||
return VectorL10n.tr("Vector", "authentication_server_selection_server_url")
|
||||
}
|
||||
/// Please read %@’s terms and policies
|
||||
public static func authenticationTermsMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_message", p1)
|
||||
}
|
||||
/// Unable to find the selected policy. Please try again later.
|
||||
public static var authenticationTermsPolicyUrlError: String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_policy_url_error")
|
||||
}
|
||||
/// Server policies
|
||||
public static var authenticationTermsTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_terms_title")
|
||||
}
|
||||
/// %@ needs to verify your account
|
||||
public static func authenticationVerifyEmailInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_input_message", p1)
|
||||
}
|
||||
/// Enter your email
|
||||
public static var authenticationVerifyEmailInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_input_title")
|
||||
}
|
||||
/// Email
|
||||
public static var authenticationVerifyEmailTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
public static var authenticationVerifyEmailWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
public static var authenticationVerifyEmailWaitingHint: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_hint")
|
||||
}
|
||||
/// Follow the instructions sent to %@
|
||||
public static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_message", p1)
|
||||
}
|
||||
/// Verify your email.
|
||||
public static var authenticationVerifyEmailWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_title")
|
||||
}
|
||||
/// %@ needs to verify your account
|
||||
public static func authenticationVerifyMsisdnInputMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_message", p1)
|
||||
}
|
||||
/// Enter your phone number
|
||||
public static var authenticationVerifyMsisdnInputTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_title")
|
||||
}
|
||||
/// Invalid phone number
|
||||
public static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_invalid_phone_number")
|
||||
}
|
||||
/// Confirmation Code
|
||||
public static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_otp_text_field_placeholder")
|
||||
}
|
||||
/// Phone Number
|
||||
public static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_text_field_placeholder")
|
||||
}
|
||||
/// Resend code
|
||||
public static var authenticationVerifyMsisdnWaitingButton: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_button")
|
||||
}
|
||||
/// A code was sent to %@
|
||||
public static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_message", p1)
|
||||
}
|
||||
/// Verify your phone number
|
||||
public static var authenticationVerifyMsisdnWaitingTitle: String {
|
||||
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_title")
|
||||
}
|
||||
/// Back
|
||||
public static var back: String {
|
||||
return VectorL10n.tr("Vector", "back")
|
||||
@@ -2811,6 +3007,14 @@ public class VectorL10n: NSObject {
|
||||
public static var locationSharingInvalidAuthorizationSettings: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings")
|
||||
}
|
||||
/// You need to have the right permissions in order to share live location in this room.
|
||||
public static var locationSharingInvalidPowerLevelMessage: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_invalid_power_level_message")
|
||||
}
|
||||
/// You don’t have permission to share live location
|
||||
public static var locationSharingInvalidPowerLevelTitle: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_invalid_power_level_title")
|
||||
}
|
||||
/// Live location error
|
||||
public static var locationSharingLiveError: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_live_error")
|
||||
@@ -3231,6 +3435,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 +4135,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 +4147,11 @@ public class VectorL10n: NSObject {
|
||||
public static var onboardingCelebrationButton: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_button")
|
||||
}
|
||||
/// Your preferences have been saved.
|
||||
/// Head to settings anytime to update your profile
|
||||
public static var onboardingCelebrationMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_message")
|
||||
}
|
||||
/// You’re all set!
|
||||
/// Looking good!
|
||||
public static var onboardingCelebrationTitle: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_celebration_title")
|
||||
}
|
||||
@@ -3951,7 +4159,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 +4251,11 @@ public class VectorL10n: NSObject {
|
||||
public static var onboardingUseCaseExistingServerMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_existing_server_message")
|
||||
}
|
||||
/// We’ll help you get connected.
|
||||
/// We’ll help you get connected
|
||||
public static var onboardingUseCaseMessage: String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_message")
|
||||
}
|
||||
/// Not sure yet? You can %@
|
||||
/// Not sure yet? %@
|
||||
public static func onboardingUseCaseNotSureYet(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "onboarding_use_case_not_sure_yet", p1)
|
||||
}
|
||||
@@ -4055,7 +4263,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 +4283,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")
|
||||
|
||||
@@ -10,230 +10,10 @@ import Foundation
|
||||
|
||||
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
|
||||
public extension VectorL10n {
|
||||
/// Your account is not created yet. Stop the registration process?
|
||||
static var authenticationCancelFlowConfirmationMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_cancel_flow_confirmation_message")
|
||||
}
|
||||
/// Make sure it’s 8 characters or more.
|
||||
static var authenticationChoosePasswordInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_message")
|
||||
}
|
||||
/// Choose a new password
|
||||
static var authenticationChoosePasswordInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_title")
|
||||
}
|
||||
/// Sign out of all devices
|
||||
static var authenticationChoosePasswordSignoutAllDevices: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_signout_all_devices")
|
||||
}
|
||||
/// Reset Password
|
||||
static var authenticationChoosePasswordSubmitButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_submit_button")
|
||||
}
|
||||
/// New Password
|
||||
static var authenticationChoosePasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_choose_password_text_field_placeholder")
|
||||
}
|
||||
/// We will send you a verification link.
|
||||
static var authenticationForgotPasswordInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_message")
|
||||
}
|
||||
/// Enter your email address
|
||||
static var authenticationForgotPasswordInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_title")
|
||||
}
|
||||
/// Email Address
|
||||
static var authenticationForgotPasswordTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
static var authenticationForgotPasswordWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
static var authenticationForgotPasswordWaitingHint: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_hint")
|
||||
}
|
||||
/// To confirm your email address, tap the button in the email we just sent to %@
|
||||
static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_message", p1)
|
||||
}
|
||||
/// Check your email
|
||||
static var authenticationForgotPasswordWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_title")
|
||||
}
|
||||
/// Forgot password
|
||||
static var authenticationLoginForgotPassword: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_forgot_password")
|
||||
}
|
||||
/// Welcome back!
|
||||
static var authenticationLoginTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_title")
|
||||
}
|
||||
/// Username or Email
|
||||
static var authenticationLoginUsername: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_login_username")
|
||||
}
|
||||
/// This server would like to make sure you are not a robot
|
||||
static var authenticationRecaptchaMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_recaptcha_message")
|
||||
}
|
||||
/// We’ll need some info to get you set up.
|
||||
static var authenticationRegistrationMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_message")
|
||||
}
|
||||
/// Must be 8 characters or more
|
||||
static var authenticationRegistrationPasswordFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_password_footer")
|
||||
}
|
||||
/// Create your account
|
||||
static var authenticationRegistrationTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_title")
|
||||
}
|
||||
/// Username
|
||||
static var authenticationRegistrationUsername: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_username")
|
||||
}
|
||||
/// You can’t change this later
|
||||
static var authenticationRegistrationUsernameFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_registration_username_footer")
|
||||
}
|
||||
/// Join millions for free on the largest public server
|
||||
static var authenticationServerInfoMatrixDescription: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_info_matrix_description")
|
||||
}
|
||||
/// Choose your server to store your data
|
||||
static var authenticationServerInfoTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_info_title")
|
||||
}
|
||||
/// Cannot find a server at this URL, please check it is correct.
|
||||
static var authenticationServerSelectionGenericError: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_generic_error")
|
||||
}
|
||||
/// What is the address of your server? A server is like a home for all your data.
|
||||
static var authenticationServerSelectionMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_message")
|
||||
}
|
||||
/// You can only connect to a server that has already been set up
|
||||
static var authenticationServerSelectionServerFooter: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_footer")
|
||||
}
|
||||
/// Server URL
|
||||
static var authenticationServerSelectionServerUrl: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_url")
|
||||
}
|
||||
/// Choose your server
|
||||
static var authenticationServerSelectionTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_server_selection_title")
|
||||
}
|
||||
/// Please read through T&C. You must accept in order to continue.
|
||||
static var authenticationTermsMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_message")
|
||||
}
|
||||
/// Unable to find the selected policy. Please try again later.
|
||||
static var authenticationTermsPolicyUrlError: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_policy_url_error")
|
||||
}
|
||||
/// Privacy policy
|
||||
static var authenticationTermsTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_terms_title")
|
||||
}
|
||||
/// This will help verify your account and enables password recovery.
|
||||
static var authenticationVerifyEmailInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
|
||||
}
|
||||
/// Enter your email address
|
||||
static var authenticationVerifyEmailInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_title")
|
||||
}
|
||||
/// Email Address
|
||||
static var authenticationVerifyEmailTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_text_field_placeholder")
|
||||
}
|
||||
/// Resend email
|
||||
static var authenticationVerifyEmailWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_button")
|
||||
}
|
||||
/// Did not receive an email?
|
||||
static var authenticationVerifyEmailWaitingHint: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_hint")
|
||||
}
|
||||
/// To confirm your email address, tap the button in the email we just sent to %@
|
||||
static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_message", p1)
|
||||
}
|
||||
/// Check your email to verify.
|
||||
static var authenticationVerifyEmailWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_title")
|
||||
}
|
||||
/// This will help verify your account and enables password recovery.
|
||||
static var authenticationVerifyMsisdnInputMessage: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_message")
|
||||
}
|
||||
/// Enter your phone number
|
||||
static var authenticationVerifyMsisdnInputTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_title")
|
||||
}
|
||||
/// Invalid phone number
|
||||
static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_invalid_phone_number")
|
||||
}
|
||||
/// Verification Code
|
||||
static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_otp_text_field_placeholder")
|
||||
}
|
||||
/// Phone Number
|
||||
static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_text_field_placeholder")
|
||||
}
|
||||
/// Resend code
|
||||
static var authenticationVerifyMsisdnWaitingButton: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_button")
|
||||
}
|
||||
/// We just sent a code to %@. Enter it below to verify it’s you.
|
||||
static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1)
|
||||
}
|
||||
/// Confirm your phone number
|
||||
static var authenticationVerifyMsisdnWaitingTitle: String {
|
||||
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_title")
|
||||
}
|
||||
/// Choose from files
|
||||
static var imagePickerActionFiles: String {
|
||||
return VectorL10n.tr("Untranslated", "image_picker_action_files")
|
||||
}
|
||||
/// Contain a lower-case letter.
|
||||
static var passwordValidationErrorContainLowercaseLetter: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_lowercase_letter")
|
||||
}
|
||||
/// Contain a number.
|
||||
static var passwordValidationErrorContainNumber: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_number")
|
||||
}
|
||||
/// Contain a symbol.
|
||||
static var passwordValidationErrorContainSymbol: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_symbol")
|
||||
}
|
||||
/// Contain an upper-case letter.
|
||||
static var passwordValidationErrorContainUppercaseLetter: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_contain_uppercase_letter")
|
||||
}
|
||||
/// Given password does not meet the criteria below:
|
||||
static var passwordValidationErrorHeader: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_header")
|
||||
}
|
||||
/// Not exceed %d characters.
|
||||
static func passwordValidationErrorMaxLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_max_length", p1)
|
||||
}
|
||||
/// At least %d characters.
|
||||
static func passwordValidationErrorMinLength(_ p1: Int) -> String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_error_min_length", p1)
|
||||
}
|
||||
/// Your password should meet the criteria below:
|
||||
static var passwordValidationInfoHeader: String {
|
||||
return VectorL10n.tr("Untranslated", "password_validation_info_header")
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ final class HomeserverConfigurationBuilder: NSObject {
|
||||
let mapStyleURL = URL(string: mapStyleURLString) {
|
||||
tileServerMapStyleURL = mapStyleURL
|
||||
} else {
|
||||
tileServerMapStyleURL = BuildSettings.tileServerMapStyleURL
|
||||
tileServerMapStyleURL = BuildSettings.defaultTileServerMapStyleURL
|
||||
}
|
||||
|
||||
let tileServerConfiguration = HomeserverTileServerConfiguration(mapStyleURL: tileServerMapStyleURL)
|
||||
|
||||
@@ -191,7 +191,7 @@ UINavigationControllerDelegate
|
||||
@param session The matrix session.
|
||||
@return Indicate NO if the key verification screen could not be presented.
|
||||
*/
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
|
||||
inSession:(MXSession*)session;
|
||||
|
||||
- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession;
|
||||
|
||||
@@ -3636,11 +3636,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
return;
|
||||
}
|
||||
|
||||
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<MXKeyVerificationTransaction *> * _Nonnull transactions) {
|
||||
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<id<MXKeyVerificationTransaction>> * _Nonnull transactions) {
|
||||
|
||||
MXLogDebug(@"[AppDelegate][MXKeyVerification] checkPendingIncomingKeyVerificationsInSession: transactions: %@", transactions);
|
||||
|
||||
for (MXKeyVerificationTransaction *transaction in transactions)
|
||||
for (id<MXKeyVerificationTransaction> transaction in transactions)
|
||||
{
|
||||
if (transaction.isIncoming)
|
||||
{
|
||||
@@ -3664,7 +3664,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
|
||||
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
|
||||
inSession:(MXSession*)session
|
||||
{
|
||||
BOOL presented = NO;
|
||||
@@ -3810,7 +3810,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
|
||||
MXKeyVerificationRequest *keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
|
||||
id<MXKeyVerificationRequest> keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
|
||||
|
||||
if ([keyVerificationRequest isKindOfClass:MXKeyVerificationByDMRequest.class])
|
||||
{
|
||||
@@ -3893,7 +3893,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
- (void)presentNewKeyVerificationRequestAlertForSession:(MXSession*)session
|
||||
senderName:(NSString*)senderName
|
||||
senderId:(NSString*)senderId
|
||||
request:(MXKeyVerificationRequest*)keyVerificationRequest
|
||||
request:(id<MXKeyVerificationRequest>)keyVerificationRequest
|
||||
{
|
||||
if (keyVerificationRequest.state != MXKeyVerificationRequestStatePending)
|
||||
{
|
||||
|
||||
+13
-22
@@ -32,7 +32,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
|
||||
enum EntryPoint {
|
||||
case registration
|
||||
case selectServerForRegistration
|
||||
case login
|
||||
}
|
||||
|
||||
@@ -131,15 +130,13 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
}
|
||||
|
||||
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
|
||||
if initialScreen != .selectServerForRegistration {
|
||||
do {
|
||||
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
|
||||
try await authenticationService.startFlow(flow, for: homeserverAddress)
|
||||
} catch {
|
||||
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
|
||||
displayError(message: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
|
||||
try await authenticationService.startFlow(flow, for: homeserverAddress)
|
||||
} catch {
|
||||
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
|
||||
displayError(message: error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
switch initialScreen {
|
||||
@@ -149,8 +146,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
} else {
|
||||
showRegistrationScreen()
|
||||
}
|
||||
case .selectServerForRegistration:
|
||||
showServerSelectionScreen()
|
||||
case .login:
|
||||
if authenticationService.state.homeserver.needsLoginFallback {
|
||||
showFallback(for: flow)
|
||||
@@ -312,6 +307,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
|
||||
// MARK: - Registration
|
||||
|
||||
#warning("Unused.")
|
||||
/// Pushes the server selection screen into the flow (other screens may also present it modally later).
|
||||
@MainActor private func showServerSelectionScreen() {
|
||||
MXLog.debug("[AuthenticationCoordinator] showServerSelectionScreen")
|
||||
@@ -398,7 +394,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
@MainActor private func showVerifyEmailScreen(registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showVerifyEmailScreen")
|
||||
|
||||
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard)
|
||||
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationVerifyEmailCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
@@ -416,11 +413,10 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
@MainActor private func showTermsScreen(terms: MXLoginTerms?, registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showTermsScreen")
|
||||
|
||||
let homeserver = authenticationService.state.homeserver
|
||||
let localizedPolicies = terms?.policiesData(forLanguage: Bundle.mxk_language(), defaultLanguage: Bundle.mxk_fallbackLanguage())
|
||||
let parameters = AuthenticationTermsCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
localizedPolicies: localizedPolicies ?? [],
|
||||
homeserverAddress: homeserver.displayableAddress)
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationTermsCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
@@ -463,7 +459,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
@MainActor private func showVerifyMSISDNScreen(registrationWizard: RegistrationWizard) {
|
||||
MXLog.debug("[AuthenticationCoordinator] showVerifyMSISDNScreen")
|
||||
|
||||
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard)
|
||||
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard,
|
||||
homeserver: authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationVerifyMsisdnCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self] result in
|
||||
self?.registrationStageDidComplete(with: result)
|
||||
@@ -788,15 +785,9 @@ extension AuthenticationCoordinator: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
// MARK: - Unused conformances
|
||||
extension AuthenticationCoordinator {
|
||||
var customServerFieldsVisible: Bool {
|
||||
get { false }
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
func update(authenticationFlow: AuthenticationFlow) {
|
||||
// unused
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AuthFallBackViewControllerDelegate
|
||||
@@ -36,9 +36,6 @@ enum AuthenticationCoordinatorResult {
|
||||
protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
|
||||
var callback: ((AuthenticationCoordinatorResult) -> Void)? { get set }
|
||||
|
||||
/// Whether the custom homeserver checkbox is enabled for the user to enter a homeserver URL.
|
||||
var customServerFieldsVisible: Bool { get set }
|
||||
|
||||
/// Update the screen to display registration or login.
|
||||
func update(authenticationFlow: AuthenticationFlow)
|
||||
|
||||
|
||||
+10
-8
@@ -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
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalize
|
||||
func senderSentTheirLocation() -> String {
|
||||
return VectorL10n.messageReplyToSenderSentTheirLocation
|
||||
}
|
||||
|
||||
func senderSentTheirLiveLocation() -> String {
|
||||
return VectorL10n.messageReplyToSenderSentTheirLiveLocation
|
||||
}
|
||||
|
||||
func messageToReplyToPrefix() -> String {
|
||||
return VectorL10n.messageReplyToMessageToReplyToPrefix
|
||||
|
||||
@@ -205,11 +205,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
if result == .customServer {
|
||||
beginAuthentication(with: .selectServerForRegistration, onStart: coordinator.stop)
|
||||
} else {
|
||||
beginAuthentication(with: .registration, onStart: coordinator.stop)
|
||||
}
|
||||
beginAuthentication(with: .registration, onStart: coordinator.stop)
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
@@ -266,8 +262,6 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
coordinator.customServerFieldsVisible = useCaseResult == .customServer
|
||||
|
||||
authenticationCoordinator = coordinator
|
||||
|
||||
coordinator.start()
|
||||
@@ -572,6 +566,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
trackSignup()
|
||||
|
||||
completion?()
|
||||
|
||||
// Reset the authentication service back to using matrix.org
|
||||
authenticationService.reset(useDefaultServer: true)
|
||||
}
|
||||
|
||||
/// Sends a signup event to the Analytics class if onboarding has completed via the register flow.
|
||||
@@ -630,7 +627,7 @@ extension OnboardingSplashScreenViewModelResult {
|
||||
|
||||
extension OnboardingUseCaseViewModelResult {
|
||||
/// The result converted into the type stored in the user session.
|
||||
var userSessionPropertyValue: UserSessionProperties.UseCase? {
|
||||
var userSessionPropertyValue: UserSessionProperties.UseCase {
|
||||
switch self {
|
||||
case .personalMessaging:
|
||||
return .personalMessaging
|
||||
@@ -640,8 +637,6 @@ extension OnboardingUseCaseViewModelResult {
|
||||
return .communityMessaging
|
||||
case .skipped:
|
||||
return .skipped
|
||||
case .customServer:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ const CGFloat kTypingCellHeight = 24;
|
||||
// Listen to location beacon received
|
||||
@property (nonatomic, weak) id beaconInfoSummaryListener;
|
||||
|
||||
// Listen to location beacon info deletion
|
||||
@property (nonatomic, weak) id beaconInfoSummaryDeletionListener;
|
||||
|
||||
// Timer used to debounce cells refresh
|
||||
@property (nonatomic, strong) NSTimer *refreshCellsTimer;
|
||||
|
||||
@@ -190,6 +193,11 @@ const CGFloat kTypingCellHeight = 24;
|
||||
[self.mxSession.aggregations.beaconAggregations removeListener:self.beaconInfoSummaryListener];
|
||||
}
|
||||
|
||||
if (self.beaconInfoSummaryDeletionListener)
|
||||
{
|
||||
[self.mxSession.aggregations.beaconAggregations removeListener:self.beaconInfoSummaryDeletionListener];
|
||||
}
|
||||
|
||||
[super destroy];
|
||||
}
|
||||
|
||||
@@ -678,7 +686,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
return roomBubbleCellData;
|
||||
}
|
||||
|
||||
- (MXKeyVerificationRequest*)keyVerificationRequestFromEventId:(NSString*)eventId
|
||||
- (id<MXKeyVerificationRequest>)keyVerificationRequestFromEventId:(NSString*)eventId
|
||||
{
|
||||
RoomBubbleCellData *roomBubbleCellData = [self roomBubbleCellDataForEventId:eventId];
|
||||
|
||||
@@ -745,7 +753,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])
|
||||
{
|
||||
@@ -770,9 +778,20 @@ const CGFloat kTypingCellHeight = 24;
|
||||
[self updateCurrentUserLocationSharingStatus];
|
||||
[self refreshFirstCellWithBeaconInfoSummary:beaconInfoSummary];
|
||||
}];
|
||||
|
||||
self.beaconInfoSummaryDeletionListener = [self.mxSession.aggregations.beaconAggregations listenToBeaconInfoSummaryDeletionInRoomWithId:self.roomId handler:^(NSString * _Nonnull beaconInfoEventId) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self updateCurrentUserLocationSharingStatus];
|
||||
[self refreshFirstCellWithBeaconInfoSummaryIdentifier:beaconInfoEventId updatedBeaconInfoSummary:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)refreshFirstCellWithBeaconInfoSummary:(id<MXBeaconInfoSummaryProtocol>)beaconInfoSummary
|
||||
{
|
||||
[self refreshFirstCellWithBeaconInfoSummaryIdentifier:beaconInfoSummary.id updatedBeaconInfoSummary:beaconInfoSummary];
|
||||
}
|
||||
|
||||
- (void)refreshFirstCellWithBeaconInfoSummaryIdentifier:(NSString*)beaconInfoEventId updatedBeaconInfoSummary:(nullable id<MXBeaconInfoSummaryProtocol>)beaconInfoSummary
|
||||
{
|
||||
NSUInteger cellIndex;
|
||||
__block RoomBubbleCellData *roomBubbleCellData;
|
||||
@@ -783,7 +802,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
if ([cellData isKindOfClass:[RoomBubbleCellData class]])
|
||||
{
|
||||
roomBubbleCellData = (RoomBubbleCellData*)cellData;
|
||||
if ([roomBubbleCellData.beaconInfoSummary.id isEqualToString:beaconInfoSummary.id])
|
||||
if ([roomBubbleCellData.beaconInfoSummary.id isEqualToString:beaconInfoEventId])
|
||||
{
|
||||
*stop = YES;
|
||||
return YES;
|
||||
@@ -927,7 +946,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 +969,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
|
||||
- (void)declineVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure
|
||||
{
|
||||
MXKeyVerificationRequest *keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
id<MXKeyVerificationRequest> keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId];
|
||||
|
||||
if (!keyVerificationRequest)
|
||||
{
|
||||
|
||||
@@ -223,6 +223,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
||||
placeholderBackground.isHidden = bannerViewData.showMap
|
||||
placeholderBackground.image = placeholderBackgroundImage
|
||||
mapView.isHidden = !bannerViewData.showMap
|
||||
attributionLabel.isHidden = !bannerViewData.showMap
|
||||
|
||||
switch bannerViewData.status {
|
||||
case .starting:
|
||||
@@ -237,7 +238,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
||||
|
||||
private func liveLocationBannerViewData(from viewState: TimelineLiveLocationViewState) -> TimelineLiveLocationViewData {
|
||||
|
||||
var status: LiveLocationSharingStatus
|
||||
let status: LiveLocationSharingStatus
|
||||
let iconTint: UIColor
|
||||
let title: String
|
||||
var titleColor: UIColor = theme.colors.primaryContent
|
||||
|
||||
@@ -329,8 +329,9 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func showLocationCoordinatorWithEvent(_ event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) {
|
||||
guard let navigationRouter = self.navigationRouter,
|
||||
let mediaManager = mxSession?.mediaManager,
|
||||
guard let mxSession = self.mxSession,
|
||||
let navigationRouter = self.navigationRouter,
|
||||
let mediaManager = mxSession.mediaManager,
|
||||
let locationContent = event.location else {
|
||||
MXLog.error("[RoomCoordinator] Invalid location showing coordinator parameters. Returning.")
|
||||
return
|
||||
@@ -348,10 +349,12 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
fatalError("[LocationSharingCoordinator] event asset type is not supported: \(coordinateType)")
|
||||
}
|
||||
|
||||
let parameters = StaticLocationViewingCoordinatorParameters(mediaManager: mediaManager,
|
||||
avatarData: avatarData,
|
||||
location: location,
|
||||
coordinateType: locationSharingCoordinatetype)
|
||||
let parameters = StaticLocationViewingCoordinatorParameters(
|
||||
session: mxSession,
|
||||
mediaManager: mediaManager,
|
||||
avatarData: avatarData,
|
||||
location: location,
|
||||
coordinateType: locationSharingCoordinatetype)
|
||||
|
||||
let coordinator = StaticLocationViewingCoordinator(parameters: parameters)
|
||||
|
||||
@@ -371,9 +374,10 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func startLocationCoordinator() {
|
||||
guard let navigationRouter = self.navigationRouter,
|
||||
let mediaManager = mxSession?.mediaManager,
|
||||
let user = mxSession?.myUser else {
|
||||
guard let mxSession = mxSession,
|
||||
let navigationRouter = self.navigationRouter,
|
||||
let mediaManager = mxSession.mediaManager,
|
||||
let user = mxSession.myUser else {
|
||||
MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.")
|
||||
return
|
||||
}
|
||||
@@ -382,7 +386,8 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
matrixItemId: user.userId,
|
||||
displayName: user.displayname)
|
||||
|
||||
let parameters = LocationSharingCoordinatorParameters(roomDataSource: roomViewController.roomDataSource,
|
||||
let parameters = LocationSharingCoordinatorParameters(session: mxSession,
|
||||
roomDataSource: roomViewController.roomDataSource,
|
||||
mediaManager: mediaManager,
|
||||
avatarData: avatarData)
|
||||
|
||||
|
||||
@@ -3683,7 +3683,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart)
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
|
||||
@@ -3719,7 +3720,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart)
|
||||
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart &&
|
||||
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
@@ -6769,7 +6771,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
||||
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
|
||||
|
||||
BOOL result = (event.eventType != MXEventTypePollStart && (!attachment || attachment.type != MXKAttachmentTypeSticker));
|
||||
BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker;
|
||||
|
||||
if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
|
||||
{
|
||||
@@ -6795,6 +6797,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
case MXEventTypeKeyVerificationMac:
|
||||
case MXEventTypeKeyVerificationDone:
|
||||
case MXEventTypeKeyVerificationCancel:
|
||||
case MXEventTypePollStart:
|
||||
case MXEventTypeBeaconInfo:
|
||||
result = NO;
|
||||
break;
|
||||
case MXEventTypeCustom:
|
||||
|
||||
@@ -130,6 +130,17 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room
|
||||
super.prepareForReuse()
|
||||
self.event = nil
|
||||
}
|
||||
|
||||
override func onLongPressGesture(_ longPressGestureRecognizer: UILongPressGestureRecognizer!) {
|
||||
|
||||
var userInfo: [String: Any]?
|
||||
|
||||
if let event = self.event {
|
||||
userInfo = [kMXKRoomBubbleCellEventKey: event]
|
||||
}
|
||||
|
||||
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellLongPressOnEvent, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocationPlainCell: RoomTimelineLocationViewDelegate {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -15,85 +15,68 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ffmpegkit
|
||||
import SwiftOGG
|
||||
|
||||
enum VoiceMessageAudioConverterError: Error {
|
||||
case generic(String)
|
||||
case conversionFailed(Error?)
|
||||
case getDurationFailed(Error?)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
struct VoiceMessageAudioConverter {
|
||||
static func convertToOpusOgg(sourceURL: URL, destinationURL: URL, completion: @escaping (Result<Void, VoiceMessageAudioConverterError>) -> Void) {
|
||||
let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a libopus -b:a 24k \"\(destinationURL.path)\""
|
||||
executeCommand(command, completion: completion)
|
||||
}
|
||||
|
||||
static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result<Void, VoiceMessageAudioConverterError>) -> Void) {
|
||||
let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a aac_at \"\(destinationURL.path)\""
|
||||
executeCommand(command, completion: completion)
|
||||
}
|
||||
|
||||
static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result<TimeInterval, VoiceMessageAudioConverterError>) -> Void) {
|
||||
FFprobeKit.getMediaInformationAsync(sourceURL.path) { session in
|
||||
guard let session = session else {
|
||||
completion(.failure(.generic("Invalid session")))
|
||||
return
|
||||
}
|
||||
|
||||
guard let returnCode = session.getReturnCode() else {
|
||||
completion(.failure(.generic("Invalid return code")))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if returnCode.isValueSuccess() {
|
||||
let mediaInfo = session.getMediaInformation()
|
||||
if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") {
|
||||
completion(.success(duration))
|
||||
} else {
|
||||
completion(.failure(.generic("Failed to get media duration")))
|
||||
}
|
||||
} else if returnCode.isValueCancel() {
|
||||
completion(.failure(.cancelled))
|
||||
} else {
|
||||
completion(.failure(.generic(String(returnCode.getValue()))))
|
||||
MXLog.error("""
|
||||
getMediaInformationAsync failed with state: \(String(describing: FFmpegKitConfig.sessionState(toString: session.getState()))), \
|
||||
returnCode: \(String(describing: returnCode)), \
|
||||
stackTrace: \(String(describing: session.getFailStackTrace()))
|
||||
""")
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try OGGConverter.convertM4aFileToOpusOGG(src: sourceURL, dest: destinationURL)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.conversionFailed(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static private func executeCommand(_ command: String, completion: @escaping (Result<Void, VoiceMessageAudioConverterError>) -> Void) {
|
||||
FFmpegKitConfig.setLogLevel(0)
|
||||
|
||||
FFmpegKit.executeAsync(command) { session in
|
||||
guard let session = session else {
|
||||
completion(.failure(.generic("Invalid session")))
|
||||
return
|
||||
}
|
||||
|
||||
guard let returnCode = session.getReturnCode() else {
|
||||
completion(.failure(.generic("Invalid return code")))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if returnCode.isValueSuccess() {
|
||||
static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result<Void, VoiceMessageAudioConverterError>) -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try OGGConverter.convertOpusOGGToM4aFile(src: sourceURL, dest: destinationURL)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
} else if returnCode.isValueCancel() {
|
||||
completion(.failure(.cancelled))
|
||||
} else {
|
||||
completion(.failure(.generic(String(returnCode.getValue()))))
|
||||
MXLog.error("""
|
||||
Failed converting voice message with state: \(String(describing: FFmpegKitConfig.sessionState(toString: session.getState()))), \
|
||||
returnCode: \(String(describing: returnCode)), \
|
||||
stackTrace: \(String(describing: session.getFailStackTrace()))
|
||||
""")
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.conversionFailed(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result<TimeInterval, VoiceMessageAudioConverterError>) -> Void) {
|
||||
let audioAsset = AVURLAsset(url: sourceURL, options: nil)
|
||||
|
||||
audioAsset.loadValuesAsynchronously(forKeys: ["duration"]) {
|
||||
var error: NSError?
|
||||
let status = audioAsset.statusOfValue(forKey: "duration", error: &error)
|
||||
|
||||
switch status {
|
||||
case .loaded:
|
||||
let duration = audioAsset.duration
|
||||
let durationInSeconds = CMTimeGetSeconds(duration)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(durationInSeconds))
|
||||
}
|
||||
case .failed:
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.getDurationFailed(error)))
|
||||
}
|
||||
case .cancelled:
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.cancelled))
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ targets:
|
||||
- package: DesignKit
|
||||
- package: Mapbox
|
||||
- package: OrderedCollections
|
||||
- package: SwiftOGG
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -68,6 +68,8 @@ enum AuthenticationChoosePasswordViewAction {
|
||||
enum AuthenticationChoosePasswordErrorType: Hashable {
|
||||
/// An error response from the homeserver.
|
||||
case mxError(String)
|
||||
/// The user hasn't tapped the link in the verification email.
|
||||
case emailNotVerified
|
||||
/// An unknown error occurred.
|
||||
case unknown
|
||||
}
|
||||
|
||||
+4
@@ -55,6 +55,10 @@ class AuthenticationChoosePasswordViewModel: AuthenticationChoosePasswordViewMod
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: VectorL10n.error,
|
||||
message: message)
|
||||
case .emailNotVerified:
|
||||
state.bindings.alertInfo = AlertInfo(id: type,
|
||||
title: VectorL10n.authenticationChoosePasswordNotVerifiedTitle,
|
||||
message: VectorL10n.authenticationChoosePasswordNotVerifiedMessage)
|
||||
case .unknown:
|
||||
state.bindings.alertInfo = AlertInfo(id: type)
|
||||
}
|
||||
|
||||
+6
-1
@@ -136,7 +136,12 @@ final class AuthenticationChoosePasswordCoordinator: Coordinator, Presentable {
|
||||
/// Processes an error to either update the flow or display it to the user.
|
||||
@MainActor private func handleError(_ error: Error) {
|
||||
if let mxError = MXError(nsError: error as NSError) {
|
||||
authenticationChoosePasswordViewModel.displayError(.mxError(mxError.error))
|
||||
if mxError.errcode == kMXErrCodeStringUnauthorized {
|
||||
authenticationChoosePasswordViewModel.displayError(.emailNotVerified)
|
||||
} else {
|
||||
authenticationChoosePasswordViewModel.displayError(.mxError(mxError.error))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+13
-28
@@ -17,31 +17,10 @@
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationChoosePasswordScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationChoosePasswordUITests(selector: #selector(verifyAuthenticationChoosePasswordScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationChoosePasswordScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationChoosePasswordScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredInvalidPassword:
|
||||
verifyEnteredInvalidPassword()
|
||||
case .enteredValidPassword:
|
||||
verifyEnteredValidPassword()
|
||||
case .enteredValidPasswordAndSignoutAllDevicesChecked:
|
||||
verifyEnteredValidPasswordAndSignoutAllDevicesChecked()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
class AuthenticationChoosePasswordUITests: MockScreenTestCase {
|
||||
func testEmptyPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.emptyPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
@@ -58,7 +37,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredInvalidPassword() {
|
||||
func testEnteredInvalidPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredInvalidPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
@@ -75,7 +56,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPassword() {
|
||||
func testEnteredValidPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
@@ -92,7 +75,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
|
||||
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
|
||||
}
|
||||
|
||||
func verifyEnteredValidPasswordAndSignoutAllDevicesChecked() {
|
||||
func testEnteredValidPasswordAndSignoutAllDevicesChecked() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPasswordAndSignoutAllDevicesChecked.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ import Foundation
|
||||
struct AuthenticationHomeserverViewData: Equatable {
|
||||
/// The homeserver string to be shown to the user.
|
||||
let address: String
|
||||
/// Whether or not the homeserver is matrix.org.
|
||||
let isMatrixDotOrg: Bool
|
||||
/// Whether or not to display the username and password text fields during login.
|
||||
let showLoginForm: Bool
|
||||
/// Whether or not to display the username and password text fields during registration.
|
||||
@@ -36,7 +34,6 @@ extension AuthenticationHomeserverViewData {
|
||||
/// A mock homeserver that is configured just like matrix.org.
|
||||
static var mockMatrixDotOrg: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "matrix.org",
|
||||
isMatrixDotOrg: true,
|
||||
showLoginForm: true,
|
||||
showRegistrationForm: true,
|
||||
ssoIdentityProviders: [
|
||||
@@ -51,7 +48,6 @@ extension AuthenticationHomeserverViewData {
|
||||
/// A mock homeserver that supports login and registration via a password but has no SSO providers.
|
||||
static var mockBasicServer: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "example.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: true,
|
||||
showRegistrationForm: true,
|
||||
ssoIdentityProviders: [])
|
||||
@@ -60,7 +56,6 @@ extension AuthenticationHomeserverViewData {
|
||||
/// A mock homeserver that supports only supports authentication via a single SSO provider.
|
||||
static var mockEnterpriseSSO: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "company.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: false,
|
||||
showRegistrationForm: false,
|
||||
ssoIdentityProviders: [SSOIdentityProvider(id: "test", name: "SAML", brand: nil, iconURL: nil)])
|
||||
@@ -69,7 +64,6 @@ extension AuthenticationHomeserverViewData {
|
||||
/// A mock homeserver that supports only supports authentication via fallback.
|
||||
static var mockFallback: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: "company.com",
|
||||
isMatrixDotOrg: false,
|
||||
showLoginForm: false,
|
||||
showRegistrationForm: false,
|
||||
ssoIdentityProviders: [])
|
||||
|
||||
@@ -27,30 +27,25 @@ struct AuthenticationServerInfoSection: View {
|
||||
// MARK: - Public
|
||||
|
||||
let address: String
|
||||
let showMatrixDotOrgInfo: Bool
|
||||
let flow: AuthenticationFlow
|
||||
let editAction: () -> Void
|
||||
|
||||
var title: String {
|
||||
flow == .login ? VectorL10n.authenticationServerInfoTitleLogin : VectorL10n.authenticationServerInfoTitle
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(VectorL10n.authenticationServerInfoTitle)
|
||||
Text(title)
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(address)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
if showMatrixDotOrgInfo {
|
||||
Text(VectorL10n.authenticationServerInfoMatrixDescription)
|
||||
.font(theme.fonts.caption1)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.accessibilityIdentifier("serverDescriptionText")
|
||||
}
|
||||
}
|
||||
Text(address)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
+3
-23
@@ -174,13 +174,14 @@ class AuthenticationService: NSObject {
|
||||
}
|
||||
|
||||
/// Reset the service to a fresh state.
|
||||
func reset() {
|
||||
/// - Parameter useDefaultServer: Pass `true` to revert back to the one in `BuildSettings`, otherwise the current homeserver will be kept.
|
||||
func reset(useDefaultServer: Bool = false) {
|
||||
loginWizard = nil
|
||||
registrationWizard = nil
|
||||
softLogoutCredentials = nil
|
||||
|
||||
// The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway.
|
||||
let address = state.homeserver.addressFromUser ?? state.homeserver.address
|
||||
let address = useDefaultServer ? BuildSettings.serverConfigDefaultHomeserverUrlString : state.homeserver.addressFromUser ?? state.homeserver.address
|
||||
let identityServer = state.identityServer
|
||||
self.state = AuthenticationState(flow: .login,
|
||||
homeserverAddress: address,
|
||||
@@ -196,27 +197,6 @@ class AuthenticationService: NSObject {
|
||||
delegate?.authenticationService(self, didReceive: token, with: transactionID) ?? false
|
||||
}
|
||||
|
||||
// /// Perform a well-known request, using the domain from the matrixId
|
||||
// func getWellKnownData(matrixId: String,
|
||||
// homeServerConnectionConfig: HomeServerConnectionConfig?) async -> WellknownResult {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /// Authenticate with a matrixId and a password
|
||||
// /// Usually call this after a successful call to getWellKnownData()
|
||||
// /// - Parameter homeServerConnectionConfig the information about the homeserver and other configuration
|
||||
// /// - Parameter matrixId the matrixId of the user
|
||||
// /// - Parameter password the password of the account
|
||||
// /// - Parameter initialDeviceName the initial device name
|
||||
// /// - Parameter deviceId the device id, optional. If not provided or null, the server will generate one.
|
||||
// func directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
// matrixId: String,
|
||||
// password: String,
|
||||
// initialDeviceName: String,
|
||||
// deviceId: String? = nil) async -> MXSession {
|
||||
//
|
||||
// }
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Query the supported login flows for the supplied homeserver.
|
||||
|
||||
@@ -65,7 +65,6 @@ struct AuthenticationState {
|
||||
/// The homeserver mapped into view data that is ready for display.
|
||||
var viewData: AuthenticationHomeserverViewData {
|
||||
AuthenticationHomeserverViewData(address: displayableAddress,
|
||||
isMatrixDotOrg: isMatrixDotOrg,
|
||||
showLoginForm: preferredLoginMode.supportsPasswordFlow,
|
||||
showRegistrationForm: registrationFlow != nil && !needsRegistrationFallback,
|
||||
ssoIdentityProviders: preferredLoginMode.ssoIdentityProviders ?? [])
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import libPhoneNumber_iOS
|
||||
|
||||
/// Set of methods to be able to login to an existing account on a homeserver.
|
||||
///
|
||||
@@ -42,11 +43,6 @@ class LoginWizard {
|
||||
self.state = State()
|
||||
}
|
||||
|
||||
// /// Get some information about a matrixId: displayName and avatar url
|
||||
// func profileInfo(for matrixID: String) async -> LoginProfileInfo {
|
||||
//
|
||||
// }
|
||||
|
||||
/// Login to the homeserver.
|
||||
/// - Parameters:
|
||||
/// - login: The login field. Can be a user name, or a msisdn (email or phone number) associated to the account.
|
||||
@@ -67,6 +63,13 @@ class LoginWizard {
|
||||
password: password,
|
||||
deviceDisplayName: initialDeviceName,
|
||||
deviceID: deviceID)
|
||||
} else if let number = try? NBPhoneNumberUtil.sharedInstance().parse(login, defaultRegion: nil),
|
||||
NBPhoneNumberUtil.sharedInstance().isValidNumber(number) {
|
||||
let msisdn = login.replacingOccurrences(of: "+", with: "")
|
||||
parameters = LoginPasswordParameters(id: .thirdParty(medium: .msisdn, address: msisdn),
|
||||
password: password,
|
||||
deviceDisplayName: initialDeviceName,
|
||||
deviceID: deviceID)
|
||||
} else {
|
||||
parameters = LoginPasswordParameters(id: .user(login),
|
||||
password: password,
|
||||
@@ -92,12 +95,6 @@ class LoginWizard {
|
||||
client: client,
|
||||
removeOtherAccounts: removeOtherAccounts)
|
||||
}
|
||||
|
||||
// /// Login to the homeserver by sending a custom JsonDict.
|
||||
// /// The data should contain at least one entry `type` with a String value.
|
||||
// func loginCustom(data: Codable) async -> MXSession {
|
||||
//
|
||||
// }
|
||||
|
||||
/// Ask the homeserver to reset the user password. The password will not be
|
||||
/// reset until `resetPasswordMailConfirmed` is successfully called.
|
||||
|
||||
+4
-4
@@ -268,17 +268,17 @@ class RegistrationWizard {
|
||||
let flowResult = authenticationSession.flowResult
|
||||
|
||||
if isCreatingAccount || isRegistrationStarted {
|
||||
return try await handleMandatoryDummyStage(flowResult: flowResult)
|
||||
return try await handleDummyStage(flowResult: flowResult)
|
||||
}
|
||||
|
||||
return .flowResponse(flowResult)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a mandatory dummy stage and handles it automatically when possible.
|
||||
private func handleMandatoryDummyStage(flowResult: FlowResult) async throws -> RegistrationResult {
|
||||
/// Checks for a dummy stage and handles it automatically when possible.
|
||||
private func handleDummyStage(flowResult: FlowResult) async throws -> RegistrationResult {
|
||||
// If the dummy stage is mandatory, do the dummy stage now
|
||||
guard flowResult.missingStages.contains(where: { $0.isDummy && $0.isMandatory }) else { return .flowResponse(flowResult) }
|
||||
guard flowResult.missingStages.contains(where: { $0.isDummy }) else { return .flowResponse(flowResult) }
|
||||
return try await dummy()
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -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
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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)
|
||||
|
||||
+6
-3
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
+12
-26
@@ -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.")
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class AuthenticationForgotPasswordViewModelTests: XCTestCase {
|
||||
var context: AuthenticationForgotPasswordViewModelType.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = AuthenticationForgotPasswordViewModel()
|
||||
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+4
-3
@@ -195,8 +195,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
|
||||
|
||||
@MainActor private func parseUsername(_ username: String) {
|
||||
guard MXTools.isMatrixUserIdentifier(username) else { return }
|
||||
let domain = username.split(separator: ":")[1]
|
||||
let homeserverAddress = HomeserverAddress.sanitized(String(domain))
|
||||
let domain = username.components(separatedBy: ":")[1]
|
||||
let homeserverAddress = HomeserverAddress.sanitized(domain)
|
||||
|
||||
startLoading(isInteractionBlocking: false)
|
||||
|
||||
@@ -260,7 +260,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
|
||||
let modalRouter = NavigationRouter()
|
||||
|
||||
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
|
||||
loginWizard: loginWizard)
|
||||
loginWizard: loginWizard,
|
||||
homeserver: parameters.authenticationService.state.homeserver)
|
||||
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
|
||||
@@ -17,57 +17,45 @@
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationLoginUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationLoginScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationLoginUITests(selector: #selector(verifyAuthenticationLoginScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationLoginScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationLoginScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
let state = "matrix.org"
|
||||
validateServerDescriptionIsVisible(for: state)
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
case .passwordOnly:
|
||||
let state = "a password only server"
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
case .passwordWithCredentials:
|
||||
let state = "a password only server with credentials entered"
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
case .ssoOnly:
|
||||
let state = "an SSO only server"
|
||||
validateServerDescriptionIsHidden(for: state)
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
case .fallback:
|
||||
let state = "a fallback server"
|
||||
validateFallback(for: state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that the server description label is shown.
|
||||
func validateServerDescriptionIsVisible(for state: String) {
|
||||
let descriptionLabel = app.staticTexts["serverDescriptionText"]
|
||||
class AuthenticationLoginUITests: MockScreenTestCase {
|
||||
func testMatrixDotOrg() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.matrixDotOrg.title)
|
||||
|
||||
XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).")
|
||||
XCTAssertEqual(descriptionLabel.label, VectorL10n.authenticationServerInfoMatrixDescription, "The server description should be correct for \(state).")
|
||||
let state = "matrix.org"
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
}
|
||||
|
||||
/// Checks that the server description label is hidden.
|
||||
func validateServerDescriptionIsHidden(for state: String) {
|
||||
let descriptionLabel = app.staticTexts["serverDescriptionText"]
|
||||
XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).")
|
||||
func testPasswordOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordOnly.title)
|
||||
|
||||
let state = "a password only server"
|
||||
validateLoginFormIsVisible(for: state)
|
||||
validateSSOButtonsAreHidden(for: state)
|
||||
|
||||
validateNextButtonIsDisabled(for: state)
|
||||
}
|
||||
|
||||
func testPasswordWithCredentials() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordWithCredentials.title)
|
||||
|
||||
let state = "a password only server with credentials entered"
|
||||
validateNextButtonIsEnabled(for: state)
|
||||
}
|
||||
|
||||
func testSSOOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.ssoOnly.title)
|
||||
|
||||
let state = "an SSO only server"
|
||||
validateLoginFormIsHidden(for: state)
|
||||
validateSSOButtonsAreShown(for: state)
|
||||
}
|
||||
|
||||
func testFallback() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.fallback.title)
|
||||
|
||||
let state = "a fallback server"
|
||||
validateFallback(for: state)
|
||||
}
|
||||
|
||||
/// Checks that the username and password text fields are shown along with the next button.
|
||||
|
||||
+3
@@ -96,6 +96,7 @@ class AuthenticationLoginViewModelTests: XCTestCase {
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be valid to submit.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
@@ -103,12 +104,14 @@ class AuthenticationLoginViewModelTests: XCTestCase {
|
||||
|
||||
// Then the view state should reflect that the homeserver is loading.
|
||||
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked from submission.")
|
||||
|
||||
// When updating the view model after loading a homeserver.
|
||||
viewModel.update(isLoading: false)
|
||||
|
||||
// Then the view state should reflect that the homeserver is now loaded.
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should once again be valid to submit.")
|
||||
}
|
||||
|
||||
@MainActor func testFallbackServer() {
|
||||
|
||||
@@ -37,15 +37,16 @@ struct AuthenticationLoginScreen: View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
.padding(.bottom, 28)
|
||||
|
||||
serverInfo
|
||||
.padding(.leading, 12)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Rectangle()
|
||||
.fill(theme.colors.quinaryContent)
|
||||
.frame(height: 1)
|
||||
.padding(.vertical, 21)
|
||||
.padding(.bottom, 22)
|
||||
|
||||
if viewModel.viewState.homeserver.showLoginForm {
|
||||
loginForm
|
||||
@@ -87,7 +88,7 @@ struct AuthenticationLoginScreen: View {
|
||||
/// The sever information section that includes a button to select a different server.
|
||||
var serverInfo: some View {
|
||||
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address,
|
||||
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
|
||||
flow: .login) {
|
||||
viewModel.send(viewAction: .selectServer)
|
||||
}
|
||||
}
|
||||
@@ -104,8 +105,7 @@ struct AuthenticationLoginScreen: View {
|
||||
onEditingChanged: usernameEditingChanged,
|
||||
onCommit: { isPasswordFocused = true })
|
||||
.accessibilityIdentifier("usernameTextField")
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
.padding(.bottom, 7)
|
||||
|
||||
RoundedBorderTextField(placeHolder: VectorL10n.authPasswordPlaceholder,
|
||||
text: $viewModel.password,
|
||||
|
||||
+1
-1
@@ -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.
|
||||
}
|
||||
|
||||
+1
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+42
-12
@@ -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.
|
||||
|
||||
+22
-7
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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)
|
||||
|
||||
+51
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+82
-61
@@ -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).")
|
||||
}
|
||||
|
||||
+134
-30
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-13
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -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.
|
||||
|
||||
+2
-1
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+9
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+29
-30
@@ -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.")
|
||||
|
||||
|
||||
+5
-5
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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 }
|
||||
|
||||
+19
-34
@@ -17,35 +17,10 @@
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationSoftLogoutScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationSoftLogoutUITests(selector: #selector(verifyAuthenticationSoftLogoutScreen))
|
||||
}
|
||||
|
||||
func verifyAuthenticationSoftLogoutScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationSoftLogoutScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .emptyPassword:
|
||||
verifyEmptyPassword()
|
||||
case .enteredPassword:
|
||||
verifyEnteredPassword()
|
||||
case .ssoOnly:
|
||||
verifySSOOnly()
|
||||
case .noSSO:
|
||||
verifyNoSSO()
|
||||
case .fallback:
|
||||
verifyFallback()
|
||||
case .noKeyBackup:
|
||||
verifyNoKeyBackup()
|
||||
}
|
||||
}
|
||||
|
||||
func verifyEmptyPassword() {
|
||||
class AuthenticationSoftLogoutUITests: MockScreenTestCase {
|
||||
func testEmptyPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.emptyPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
@@ -77,7 +52,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyEnteredPassword() {
|
||||
func testEnteredPassword() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.enteredPassword.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
@@ -109,7 +86,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifySSOOnly() {
|
||||
func testSSOOnly() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.ssoOnly.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
@@ -138,7 +117,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoSSO() {
|
||||
func testNoSSO() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noSSO.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
@@ -167,7 +148,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyFallback() {
|
||||
func testFallback() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.fallback.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
|
||||
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
|
||||
@@ -197,7 +180,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
|
||||
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
|
||||
}
|
||||
|
||||
func verifyNoKeyBackup() {
|
||||
func testNoKeyBackup() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noKeyBackup.title)
|
||||
|
||||
XCTAssertFalse(app.staticTexts["messageLabel2"].exists, "The message 2 should not be shown.")
|
||||
}
|
||||
|
||||
|
||||
@@ -44,9 +44,15 @@ enum AuthenticationTermsViewModelResult {
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationTermsViewState: BindableState {
|
||||
/// The homeserver asking the user to accept the terms.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationTermsBindings
|
||||
|
||||
var headerMessage: String {
|
||||
VectorL10n.authenticationTermsMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether or not all of the policies have been accepted.
|
||||
var hasAcceptedAllPolicies: Bool {
|
||||
bindings.policies.allSatisfy(\.accepted)
|
||||
|
||||
@@ -33,8 +33,9 @@ class AuthenticationTermsViewModel: AuthenticationTermsViewModelType, Authentica
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(policies: [AuthenticationTermsPolicy]) {
|
||||
super.init(initialViewState: AuthenticationTermsViewState(bindings: AuthenticationTermsBindings(policies: policies)))
|
||||
init(homeserver: AuthenticationHomeserverViewData, policies: [AuthenticationTermsPolicy]) {
|
||||
super.init(initialViewState: AuthenticationTermsViewState(homeserver: homeserver,
|
||||
bindings: AuthenticationTermsBindings(policies: policies)))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
+4
-4
@@ -22,8 +22,8 @@ struct AuthenticationTermsCoordinatorParameters {
|
||||
let registrationWizard: RegistrationWizard
|
||||
/// The policies to be accepted by the user.
|
||||
let localizedPolicies: [MXLoginPolicyData]
|
||||
/// The address of the homeserver (shown beneath the policies).
|
||||
let homeserverAddress: String
|
||||
/// The homeserver that provided the policies.
|
||||
let homeserver: AuthenticationState.Homeserver
|
||||
}
|
||||
|
||||
final class AuthenticationTermsCoordinator: Coordinator, Presentable {
|
||||
@@ -59,10 +59,10 @@ final class AuthenticationTermsCoordinator: Coordinator, Presentable {
|
||||
@MainActor init(parameters: AuthenticationTermsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let subtitle = parameters.homeserverAddress
|
||||
let subtitle = parameters.homeserver.displayableAddress
|
||||
let policies = parameters.localizedPolicies.compactMap { AuthenticationTermsPolicy(url: $0.url, title: $0.name, subtitle: subtitle) }
|
||||
|
||||
let viewModel = AuthenticationTermsViewModel(policies: policies)
|
||||
let viewModel = AuthenticationTermsViewModel(homeserver: parameters.homeserver.viewData, policies: policies)
|
||||
let view = AuthenticationTermsScreen(viewModel: viewModel.context)
|
||||
authenticationTermsViewModel = viewModel
|
||||
authenticationTermsHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
@@ -37,16 +37,18 @@ enum MockAuthenticationTermsScreenState: MockScreenState, CaseIterable {
|
||||
let viewModel: AuthenticationTermsViewModel
|
||||
switch self {
|
||||
case .matrixDotOrg:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
|
||||
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
subtitle: "matrix.org")])
|
||||
case .accepted:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
|
||||
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
|
||||
title: "Terms and Conditions",
|
||||
subtitle: "matrix.org",
|
||||
accepted: true)])
|
||||
case .multiple:
|
||||
viewModel = AuthenticationTermsViewModel(policies: [
|
||||
viewModel = AuthenticationTermsViewModel(homeserver: .mockBasicServer, policies: [
|
||||
AuthenticationTermsPolicy(url: "https://example.com/terms", title: "Terms and Conditions", subtitle: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/privacy", title: "Privacy Policy", subtitle: "example.com"),
|
||||
AuthenticationTermsPolicy(url: "https://example.com/conduct", title: "Code of Conduct", subtitle: "example.com")
|
||||
|
||||
@@ -17,26 +17,20 @@
|
||||
import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
class AuthenticationTermsUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockAuthenticationTermsScreenState.self
|
||||
class AuthenticationTermsUITests: MockScreenTestCase {
|
||||
func testMatrixDotOrg() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.matrixDotOrg.title)
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return AuthenticationTermsUITests(selector: #selector(verifyAuthenticationTermsScreen))
|
||||
|
||||
func testAccepted() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.accepted.title)
|
||||
verifyTerms(accepted: true)
|
||||
}
|
||||
|
||||
func verifyAuthenticationTermsScreen() throws {
|
||||
guard let screenState = screenState as? MockAuthenticationTermsScreenState else { fatalError("no screen") }
|
||||
switch screenState {
|
||||
case .matrixDotOrg:
|
||||
verifyTerms(accepted: false)
|
||||
case .accepted:
|
||||
verifyTerms(accepted: true)
|
||||
case .multiple:
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
func testMultiple() {
|
||||
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.multiple.title)
|
||||
verifyTerms(accepted: false)
|
||||
}
|
||||
|
||||
func verifyTerms(accepted: Bool) {
|
||||
|
||||
@@ -62,7 +62,7 @@ struct AuthenticationTermsScreen: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
Text(VectorL10n.authenticationTermsMessage)
|
||||
Text(viewModel.viewState.headerMessage)
|
||||
.font(theme.fonts.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
||||
@@ -32,11 +32,18 @@ enum AuthenticationVerifyEmailViewModelResult {
|
||||
// MARK: View
|
||||
|
||||
struct AuthenticationVerifyEmailViewState: BindableState {
|
||||
/// The homeserver requesting email verification.
|
||||
let homeserver: AuthenticationHomeserverViewData
|
||||
/// An email has been sent and the app is waiting for the user to tap the link.
|
||||
var hasSentEmail = false
|
||||
/// View state that can be bound to from SwiftUI.
|
||||
var bindings: AuthenticationVerifyEmailBindings
|
||||
|
||||
/// The message shown in the header while asking for an email address to be entered.
|
||||
var formHeaderMessage: String {
|
||||
VectorL10n.authenticationVerifyEmailInputMessage(homeserver.address)
|
||||
}
|
||||
|
||||
/// Whether the email address is valid and the user can continue.
|
||||
var hasInvalidAddress: Bool {
|
||||
bindings.emailAddress.isEmpty
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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)
|
||||
@@ -108,7 +110,7 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
|
||||
|
||||
/// Sends a validation email to the supplied address and then begins polling the server.
|
||||
@MainActor private func sendEmail(_ address: String) {
|
||||
let threePID = RegisterThreePID.email(address)
|
||||
let threePID = RegisterThreePID.email(address.trimmingCharacters(in: .whitespaces))
|
||||
|
||||
startLoading()
|
||||
|
||||
|
||||
+6
-3
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
+12
-26
@@ -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.")
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase {
|
||||
var context: AuthenticationVerifyEmailViewModelType.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = AuthenticationVerifyEmailViewModel()
|
||||
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user