Merge pull request #6480 from vector-im/release/1.8.24/release

Release 1.8.24
This commit is contained in:
ismailgulek
2022-07-26 15:16:09 +03:00
committed by GitHub
154 changed files with 1781 additions and 1721 deletions
+1 -8
View File
@@ -44,14 +44,7 @@ jobs:
name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
contains(github.event.issue.labels.*.name, 'X-Needs-Design')
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
+38
View File
@@ -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
+2 -2
View File
@@ -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
+4 -3
View File
@@ -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
+1 -1
View File
@@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
$matrixSDKVersion = '= 0.23.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",
-69
View File
@@ -19,72 +19,3 @@
// MARK: Onboarding Personalization WIP
"image_picker_action_files" = "Choose from files";
// MARK: Onboarding Authentication WIP
"authentication_registration_title" = "Create your account";
"authentication_registration_message" = "Well need some info to get you set up.";
"authentication_registration_username" = "Username";
"authentication_registration_username_footer" = "You cant change this later";
"authentication_registration_password_footer" = "Must be 8 characters or more";
"authentication_login_title" = "Welcome back!";
"authentication_login_username" = "Username or Email";
"authentication_login_forgot_password" = "Forgot password";
"authentication_server_info_title" = "Choose your server to store your data";
"authentication_server_info_matrix_description" = "Join millions for free on the largest public server";
"authentication_server_selection_title" = "Choose your server";
"authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data.";
"authentication_server_selection_server_url" = "Server URL";
"authentication_server_selection_server_footer" = "You can only connect to a server that has already been set up";
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
"authentication_verify_email_input_title" = "Enter your email address";
"authentication_verify_email_input_message" = "This will help verify your account and enables password recovery.";
"authentication_verify_email_text_field_placeholder" = "Email Address";
"authentication_verify_email_waiting_title" = "Check your email to verify.";
"authentication_verify_email_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
"authentication_verify_email_waiting_button" = "Resend email";
"authentication_forgot_password_input_title" = "Enter your email address";
"authentication_forgot_password_input_message" = "We will send you a verification link.";
"authentication_forgot_password_text_field_placeholder" = "Email Address";
"authentication_forgot_password_waiting_title" = "Check your email";
"authentication_forgot_password_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@";
"authentication_forgot_password_waiting_hint" = "Did not receive an email?";
"authentication_forgot_password_waiting_button" = "Resend email";
"authentication_choose_password_input_title" = "Choose a new password";
"authentication_choose_password_input_message" = "Make sure its 8 characters or more.";
"authentication_choose_password_text_field_placeholder" = "New Password";
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
"authentication_choose_password_submit_button" = "Reset Password";
"authentication_verify_msisdn_input_title" = "Enter your phone number";
"authentication_verify_msisdn_input_message" = "This will help verify your account and enables password recovery.";
"authentication_verify_msisdn_text_field_placeholder" = "Phone Number";
"authentication_verify_msisdn_otp_text_field_placeholder" = "Verification Code";
"authentication_verify_msisdn_waiting_title" = "Confirm your phone number";
"authentication_verify_msisdn_waiting_message" = "We just sent a code to %@. Enter it below to verify its you.";
"authentication_verify_msisdn_waiting_button" = "Resend code";
"authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number";
"authentication_terms_title" = "Privacy policy";
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
"authentication_terms_policy_url_error" = "Unable to find the selected policy. Please try again later.";
"authentication_recaptcha_message" = "This server would like to make sure you are not a robot";
// MARK: Password Validation
"password_validation_info_header" = "Your password should meet the criteria below:";
"password_validation_error_header" = "Given password does not meet the criteria below:";
"password_validation_error_min_length" = "At least %d characters.";
"password_validation_error_max_length" = "Not exceed %d characters.";
"password_validation_error_contain_lowercase_letter" = "Contain a lower-case letter.";
"password_validation_error_contain_uppercase_letter" = "Contain an upper-case letter.";
"password_validation_error_contain_number" = "Contain a number.";
"password_validation_error_contain_symbol" = "Contain a symbol.";
+94 -9
View File
@@ -96,7 +96,7 @@
"accessibility_checkbox_label" = "checkbox";
"accessibility_button_label" = "button";
// Onboarding
// MARK: Onboarding
"onboarding_splash_register_button_title" = "Create account";
"onboarding_splash_login_button_title" = "I already have an account";
"onboarding_splash_page_1_title" = "Own your conversations.";
@@ -109,19 +109,19 @@
"onboarding_splash_page_4_message" = "Element is also great for the workplace. Its trusted by the worlds most secure organisations.";
"onboarding_use_case_title" = "Who will you chat to the most?";
"onboarding_use_case_message" = "Well help you get connected.";
"onboarding_use_case_message" = "Well help you get connected";
"onboarding_use_case_personal_messaging" = "Friends and family";
"onboarding_use_case_work_messaging" = "Teams";
"onboarding_use_case_community_messaging" = "Communities";
/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */
"onboarding_use_case_not_sure_yet" = "Not sure yet? You can %@";
"onboarding_use_case_skip_button" = "skip this question";
"onboarding_use_case_not_sure_yet" = "Not sure yet? %@";
"onboarding_use_case_skip_button" = "Skip this question";
"onboarding_use_case_existing_server_message" = "Looking to join an existing server?";
"onboarding_use_case_existing_server_button" = "Connect to server";
"onboarding_congratulations_title" = "Congratulations!";
/* The placeholder string contains the user's matrix ID */
"onboarding_congratulations_message" = "Your account %@ has been created.";
"onboarding_congratulations_message" = "Your account %@ has been created";
"onboarding_congratulations_personalize_button" = "Personalise profile";
"onboarding_congratulations_home_button" = "Take me home";
@@ -135,14 +135,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" = "Youre all set!";
"onboarding_celebration_message" = "Your preferences have been saved.";
"onboarding_celebration_title" = "Looking good!";
"onboarding_celebration_message" = "Head to settings anytime to update your profile";
"onboarding_celebration_button" = "Let's go";
// Authentication
// MARK: Authentication
"authentication_registration_title" = "Create your account";
"authentication_registration_username" = "Username";
"authentication_registration_username_footer" = "You cant change this later";
/* The placeholder will show the full Matrix ID that has been entered. */
"authentication_registration_username_footer_available" = "Others can discover you %@";
"authentication_registration_password_footer" = "Must be 8 characters or more";
"authentication_server_info_title" = "Where your conversations will live";
"authentication_login_title" = "Welcome back!";
"authentication_login_username" = "Username / Email / Phone";
"authentication_login_forgot_password" = "Forgot password";
"authentication_server_info_title_login" = "Where your conversations live";
"authentication_server_selection_login_title" = "Connect to homeserver";
"authentication_server_selection_login_message" = "What is the address of your server?";
"authentication_server_selection_register_title" = "Select your homeserver";
"authentication_server_selection_register_message" = "What is the address of your server? This is like a home for all your data";
"authentication_server_selection_server_url" = "Homeserver URL";
"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct.";
"authentication_cancel_flow_confirmation_message" = "Your account is not created yet. Stop the registration process?";
"authentication_verify_email_input_title" = "Enter your email";
/* The placeholder will show the homeserver's domain */
"authentication_verify_email_input_message" = "%@ needs to verify your account";
"authentication_verify_email_text_field_placeholder" = "Email";
"authentication_verify_email_waiting_title" = "Verify your email.";
/* The placeholder will show the email address that was entered. */
"authentication_verify_email_waiting_message" = "Follow the instructions sent to %@";
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
"authentication_verify_email_waiting_button" = "Resend email";
"authentication_forgot_password_input_title" = "Enter your email";
/* The placeholder will show the homeserver's domain */
"authentication_forgot_password_input_message" = "%@ will send you a verification link";
"authentication_forgot_password_text_field_placeholder" = "Email";
"authentication_forgot_password_waiting_title" = "Check your email.";
/* The placeholder will show the email address that was entered. */
"authentication_forgot_password_waiting_message" = "Follow the instructions sent to %@";
"authentication_forgot_password_waiting_button" = "Resend email";
"authentication_choose_password_input_title" = "Choose a new password";
"authentication_choose_password_input_message" = "Make sure its 8 characters or more";
"authentication_choose_password_text_field_placeholder" = "New Password";
"authentication_choose_password_signout_all_devices" = "Sign out of all devices";
"authentication_choose_password_submit_button" = "Reset Password";
"authentication_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 dont 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
+138 -180
View File
@@ -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/>
+247 -7
View File
@@ -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 its 8 characters or more
public static var authenticationChoosePasswordInputMessage: String {
return VectorL10n.tr("Vector", "authentication_choose_password_input_message")
}
/// Choose a new password
public static var authenticationChoosePasswordInputTitle: String {
return VectorL10n.tr("Vector", "authentication_choose_password_input_title")
}
/// 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 cant change this later
public static var authenticationRegistrationUsernameFooter: String {
return VectorL10n.tr("Vector", "authentication_registration_username_footer")
}
/// Others can discover you %@
public static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_registration_username_footer_available", p1)
}
/// Where your conversations will live
public static var authenticationServerInfoTitle: String {
return VectorL10n.tr("Vector", "authentication_server_info_title")
}
/// Where your conversations live
public static var authenticationServerInfoTitleLogin: String {
return VectorL10n.tr("Vector", "authentication_server_info_title_login")
}
/// Cannot find a server at this URL, please check it is correct.
public static var authenticationServerSelectionGenericError: String {
return VectorL10n.tr("Vector", "authentication_server_selection_generic_error")
}
/// What is the address of your server?
public static var authenticationServerSelectionLoginMessage: String {
return VectorL10n.tr("Vector", "authentication_server_selection_login_message")
}
/// Connect to homeserver
public static var authenticationServerSelectionLoginTitle: String {
return VectorL10n.tr("Vector", "authentication_server_selection_login_title")
}
/// What is the address of your server? This is like a home for all your data
public static var authenticationServerSelectionRegisterMessage: String {
return VectorL10n.tr("Vector", "authentication_server_selection_register_message")
}
/// Select your homeserver
public static var authenticationServerSelectionRegisterTitle: String {
return VectorL10n.tr("Vector", "authentication_server_selection_register_title")
}
/// Homeserver URL
public static var authenticationServerSelectionServerUrl: String {
return VectorL10n.tr("Vector", "authentication_server_selection_server_url")
}
/// Please read %@s terms and policies
public static func authenticationTermsMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_terms_message", p1)
}
/// Unable to find the selected policy. Please try again later.
public static var authenticationTermsPolicyUrlError: String {
return VectorL10n.tr("Vector", "authentication_terms_policy_url_error")
}
/// Server policies
public static var authenticationTermsTitle: String {
return VectorL10n.tr("Vector", "authentication_terms_title")
}
/// %@ needs to verify your account
public static func authenticationVerifyEmailInputMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_verify_email_input_message", p1)
}
/// Enter your email
public static var authenticationVerifyEmailInputTitle: String {
return VectorL10n.tr("Vector", "authentication_verify_email_input_title")
}
/// Email
public static var authenticationVerifyEmailTextFieldPlaceholder: String {
return VectorL10n.tr("Vector", "authentication_verify_email_text_field_placeholder")
}
/// Resend email
public static var authenticationVerifyEmailWaitingButton: String {
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_button")
}
/// Did not receive an email?
public static var authenticationVerifyEmailWaitingHint: String {
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_hint")
}
/// Follow the instructions sent to %@
public static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_message", p1)
}
/// Verify your email.
public static var authenticationVerifyEmailWaitingTitle: String {
return VectorL10n.tr("Vector", "authentication_verify_email_waiting_title")
}
/// %@ needs to verify your account
public static func authenticationVerifyMsisdnInputMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_message", p1)
}
/// Enter your phone number
public static var authenticationVerifyMsisdnInputTitle: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_input_title")
}
/// Invalid phone number
public static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_invalid_phone_number")
}
/// Confirmation Code
public static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_otp_text_field_placeholder")
}
/// Phone Number
public static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_text_field_placeholder")
}
/// Resend code
public static var authenticationVerifyMsisdnWaitingButton: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_button")
}
/// A code was sent to %@
public static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_message", p1)
}
/// Verify your phone number
public static var authenticationVerifyMsisdnWaitingTitle: String {
return VectorL10n.tr("Vector", "authentication_verify_msisdn_waiting_title")
}
/// Back
public static var back: String {
return VectorL10n.tr("Vector", "back")
@@ -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 dont 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")
}
/// Youre 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")
}
/// Well help you get connected.
/// Well help you get connected
public static var onboardingUseCaseMessage: String {
return VectorL10n.tr("Vector", "onboarding_use_case_message")
}
/// Not sure yet? You can %@
/// Not sure yet? %@
public static func onboardingUseCaseNotSureYet(_ p1: String) -> String {
return VectorL10n.tr("Vector", "onboarding_use_case_not_sure_yet", p1)
}
@@ -4055,7 +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")
-220
View File
@@ -10,230 +10,10 @@ import Foundation
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
public extension VectorL10n {
/// Your account is not created yet. Stop the registration process?
static var authenticationCancelFlowConfirmationMessage: String {
return VectorL10n.tr("Untranslated", "authentication_cancel_flow_confirmation_message")
}
/// Make sure its 8 characters or more.
static var authenticationChoosePasswordInputMessage: String {
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_message")
}
/// Choose a new password
static var authenticationChoosePasswordInputTitle: String {
return VectorL10n.tr("Untranslated", "authentication_choose_password_input_title")
}
/// Sign out of all devices
static var authenticationChoosePasswordSignoutAllDevices: String {
return VectorL10n.tr("Untranslated", "authentication_choose_password_signout_all_devices")
}
/// Reset Password
static var authenticationChoosePasswordSubmitButton: String {
return VectorL10n.tr("Untranslated", "authentication_choose_password_submit_button")
}
/// New Password
static var authenticationChoosePasswordTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_choose_password_text_field_placeholder")
}
/// We will send you a verification link.
static var authenticationForgotPasswordInputMessage: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_message")
}
/// Enter your email address
static var authenticationForgotPasswordInputTitle: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_title")
}
/// Email Address
static var authenticationForgotPasswordTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_text_field_placeholder")
}
/// Resend email
static var authenticationForgotPasswordWaitingButton: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_button")
}
/// Did not receive an email?
static var authenticationForgotPasswordWaitingHint: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_hint")
}
/// To confirm your email address, tap the button in the email we just sent to %@
static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_message", p1)
}
/// Check your email
static var authenticationForgotPasswordWaitingTitle: String {
return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_title")
}
/// Forgot password
static var authenticationLoginForgotPassword: String {
return VectorL10n.tr("Untranslated", "authentication_login_forgot_password")
}
/// Welcome back!
static var authenticationLoginTitle: String {
return VectorL10n.tr("Untranslated", "authentication_login_title")
}
/// Username or Email
static var authenticationLoginUsername: String {
return VectorL10n.tr("Untranslated", "authentication_login_username")
}
/// This server would like to make sure you are not a robot
static var authenticationRecaptchaMessage: String {
return VectorL10n.tr("Untranslated", "authentication_recaptcha_message")
}
/// Well need some info to get you set up.
static var authenticationRegistrationMessage: String {
return VectorL10n.tr("Untranslated", "authentication_registration_message")
}
/// Must be 8 characters or more
static var authenticationRegistrationPasswordFooter: String {
return VectorL10n.tr("Untranslated", "authentication_registration_password_footer")
}
/// Create your account
static var authenticationRegistrationTitle: String {
return VectorL10n.tr("Untranslated", "authentication_registration_title")
}
/// Username
static var authenticationRegistrationUsername: String {
return VectorL10n.tr("Untranslated", "authentication_registration_username")
}
/// You cant change this later
static var authenticationRegistrationUsernameFooter: String {
return VectorL10n.tr("Untranslated", "authentication_registration_username_footer")
}
/// Join millions for free on the largest public server
static var authenticationServerInfoMatrixDescription: String {
return VectorL10n.tr("Untranslated", "authentication_server_info_matrix_description")
}
/// Choose your server to store your data
static var authenticationServerInfoTitle: String {
return VectorL10n.tr("Untranslated", "authentication_server_info_title")
}
/// Cannot find a server at this URL, please check it is correct.
static var authenticationServerSelectionGenericError: String {
return VectorL10n.tr("Untranslated", "authentication_server_selection_generic_error")
}
/// What is the address of your server? A server is like a home for all your data.
static var authenticationServerSelectionMessage: String {
return VectorL10n.tr("Untranslated", "authentication_server_selection_message")
}
/// You can only connect to a server that has already been set up
static var authenticationServerSelectionServerFooter: String {
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_footer")
}
/// Server URL
static var authenticationServerSelectionServerUrl: String {
return VectorL10n.tr("Untranslated", "authentication_server_selection_server_url")
}
/// Choose your server
static var authenticationServerSelectionTitle: String {
return VectorL10n.tr("Untranslated", "authentication_server_selection_title")
}
/// Please read through T&C. You must accept in order to continue.
static var authenticationTermsMessage: String {
return VectorL10n.tr("Untranslated", "authentication_terms_message")
}
/// Unable to find the selected policy. Please try again later.
static var authenticationTermsPolicyUrlError: String {
return VectorL10n.tr("Untranslated", "authentication_terms_policy_url_error")
}
/// Privacy policy
static var authenticationTermsTitle: String {
return VectorL10n.tr("Untranslated", "authentication_terms_title")
}
/// This will help verify your account and enables password recovery.
static var authenticationVerifyEmailInputMessage: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message")
}
/// Enter your email address
static var authenticationVerifyEmailInputTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_input_title")
}
/// Email Address
static var authenticationVerifyEmailTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_text_field_placeholder")
}
/// Resend email
static var authenticationVerifyEmailWaitingButton: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_button")
}
/// Did not receive an email?
static var authenticationVerifyEmailWaitingHint: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_hint")
}
/// To confirm your email address, tap the button in the email we just sent to %@
static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_message", p1)
}
/// Check your email to verify.
static var authenticationVerifyEmailWaitingTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_email_waiting_title")
}
/// This will help verify your account and enables password recovery.
static var authenticationVerifyMsisdnInputMessage: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_message")
}
/// Enter your phone number
static var authenticationVerifyMsisdnInputTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_title")
}
/// Invalid phone number
static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_invalid_phone_number")
}
/// Verification Code
static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_otp_text_field_placeholder")
}
/// Phone Number
static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_text_field_placeholder")
}
/// Resend code
static var authenticationVerifyMsisdnWaitingButton: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_button")
}
/// We just sent a code to %@. Enter it below to verify its you.
static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1)
}
/// Confirm your phone number
static var authenticationVerifyMsisdnWaitingTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_title")
}
/// Choose from files
static var imagePickerActionFiles: String {
return VectorL10n.tr("Untranslated", "image_picker_action_files")
}
/// Contain a lower-case letter.
static var passwordValidationErrorContainLowercaseLetter: String {
return VectorL10n.tr("Untranslated", "password_validation_error_contain_lowercase_letter")
}
/// Contain a number.
static var passwordValidationErrorContainNumber: String {
return VectorL10n.tr("Untranslated", "password_validation_error_contain_number")
}
/// Contain a symbol.
static var passwordValidationErrorContainSymbol: String {
return VectorL10n.tr("Untranslated", "password_validation_error_contain_symbol")
}
/// Contain an upper-case letter.
static var passwordValidationErrorContainUppercaseLetter: String {
return VectorL10n.tr("Untranslated", "password_validation_error_contain_uppercase_letter")
}
/// Given password does not meet the criteria below:
static var passwordValidationErrorHeader: String {
return VectorL10n.tr("Untranslated", "password_validation_error_header")
}
/// Not exceed %d characters.
static func passwordValidationErrorMaxLength(_ p1: Int) -> String {
return VectorL10n.tr("Untranslated", "password_validation_error_max_length", p1)
}
/// At least %d characters.
static func passwordValidationErrorMinLength(_ p1: Int) -> String {
return VectorL10n.tr("Untranslated", "password_validation_error_min_length", p1)
}
/// Your password should meet the criteria below:
static var passwordValidationInfoHeader: String {
return VectorL10n.tr("Untranslated", "password_validation_info_header")
}
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
@@ -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)
+1 -1
View File
@@ -191,7 +191,7 @@ UINavigationControllerDelegate
@param session The matrix session.
@return Indicate NO if the key verification screen could not be presented.
*/
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
inSession:(MXSession*)session;
- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession;
+5 -5
View File
@@ -3636,11 +3636,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
return;
}
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<MXKeyVerificationTransaction *> * _Nonnull transactions) {
[mxSession.crypto.keyVerificationManager transactions:^(NSArray<id<MXKeyVerificationTransaction>> * _Nonnull transactions) {
MXLogDebug(@"[AppDelegate][MXKeyVerification] checkPendingIncomingKeyVerificationsInSession: transactions: %@", transactions);
for (MXKeyVerificationTransaction *transaction in transactions)
for (id<MXKeyVerificationTransaction> transaction in transactions)
{
if (transaction.isIncoming)
{
@@ -3664,7 +3664,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
}
}
- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest
- (BOOL)presentIncomingKeyVerificationRequest:(id<MXKeyVerificationRequest>)incomingKeyVerificationRequest
inSession:(MXSession*)session
{
BOOL presented = NO;
@@ -3810,7 +3810,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
NSDictionary *userInfo = notification.userInfo;
MXKeyVerificationRequest *keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
id<MXKeyVerificationRequest> keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey];
if ([keyVerificationRequest isKindOfClass:MXKeyVerificationByDMRequest.class])
{
@@ -3893,7 +3893,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
- (void)presentNewKeyVerificationRequestAlertForSession:(MXSession*)session
senderName:(NSString*)senderName
senderId:(NSString*)senderId
request:(MXKeyVerificationRequest*)keyVerificationRequest
request:(id<MXKeyVerificationRequest>)keyVerificationRequest
{
if (keyVerificationRequest.state != MXKeyVerificationRequestStatePending)
{
@@ -32,7 +32,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
enum EntryPoint {
case registration
case selectServerForRegistration
case login
}
@@ -131,15 +130,13 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
}
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
if initialScreen != .selectServerForRegistration {
do {
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
try await authenticationService.startFlow(flow, for: homeserverAddress)
} catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
displayError(message: error.localizedDescription)
return
}
do {
let homeserverAddress = authenticationService.state.homeserver.addressFromUser ?? authenticationService.state.homeserver.address
try await authenticationService.startFlow(flow, for: homeserverAddress)
} catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start")
displayError(message: error.localizedDescription)
return
}
switch initialScreen {
@@ -149,8 +146,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
} else {
showRegistrationScreen()
}
case .selectServerForRegistration:
showServerSelectionScreen()
case .login:
if authenticationService.state.homeserver.needsLoginFallback {
showFallback(for: flow)
@@ -312,6 +307,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
// MARK: - Registration
#warning("Unused.")
/// Pushes the server selection screen into the flow (other screens may also present it modally later).
@MainActor private func showServerSelectionScreen() {
MXLog.debug("[AuthenticationCoordinator] showServerSelectionScreen")
@@ -398,7 +394,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
@MainActor private func showVerifyEmailScreen(registrationWizard: RegistrationWizard) {
MXLog.debug("[AuthenticationCoordinator] showVerifyEmailScreen")
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard)
let parameters = AuthenticationVerifyEmailCoordinatorParameters(registrationWizard: registrationWizard,
homeserver: authenticationService.state.homeserver)
let coordinator = AuthenticationVerifyEmailCoordinator(parameters: parameters)
coordinator.callback = { [weak self] result in
self?.registrationStageDidComplete(with: result)
@@ -416,11 +413,10 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
@MainActor private func showTermsScreen(terms: MXLoginTerms?, registrationWizard: RegistrationWizard) {
MXLog.debug("[AuthenticationCoordinator] showTermsScreen")
let homeserver = authenticationService.state.homeserver
let localizedPolicies = terms?.policiesData(forLanguage: Bundle.mxk_language(), defaultLanguage: Bundle.mxk_fallbackLanguage())
let parameters = AuthenticationTermsCoordinatorParameters(registrationWizard: registrationWizard,
localizedPolicies: localizedPolicies ?? [],
homeserverAddress: homeserver.displayableAddress)
homeserver: authenticationService.state.homeserver)
let coordinator = AuthenticationTermsCoordinator(parameters: parameters)
coordinator.callback = { [weak self] result in
self?.registrationStageDidComplete(with: result)
@@ -463,7 +459,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
@MainActor private func showVerifyMSISDNScreen(registrationWizard: RegistrationWizard) {
MXLog.debug("[AuthenticationCoordinator] showVerifyMSISDNScreen")
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard)
let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard,
homeserver: authenticationService.state.homeserver)
let coordinator = AuthenticationVerifyMsisdnCoordinator(parameters: parameters)
coordinator.callback = { [weak self] result in
self?.registrationStageDidComplete(with: result)
@@ -788,15 +785,9 @@ extension AuthenticationCoordinator: UIAdaptivePresentationControllerDelegate {
// MARK: - Unused conformances
extension AuthenticationCoordinator {
var customServerFieldsVisible: Bool {
get { false }
set { /* no-op */ }
}
func update(authenticationFlow: AuthenticationFlow) {
// unused
}
}
// MARK: - AuthFallBackViewControllerDelegate
@@ -36,9 +36,6 @@ enum AuthenticationCoordinatorResult {
protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
var callback: ((AuthenticationCoordinatorResult) -> Void)? { get set }
/// Whether the custom homeserver checkbox is enabled for the user to enter a homeserver URL.
var customServerFieldsVisible: Bool { get set }
/// Update the screen to display registration or login.
func update(authenticationFlow: AuthenticationFlow)
@@ -48,13 +48,6 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
var childCoordinators: [Coordinator] = []
var callback: ((AuthenticationCoordinatorResult) -> Void)?
var customServerFieldsVisible = false {
didSet {
guard customServerFieldsVisible != oldValue else { return }
authenticationViewController.setCustomServerFieldsVisible(customServerFieldsVisible)
}
}
// MARK: - Setup
init(parameters: LegacyAuthenticationCoordinatorParameters) {
@@ -78,7 +71,16 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
authenticationViewController.authVCDelegate = self
// Set (or clear) any soft-logout credentials.
authenticationViewController.softLogoutCredentials = authenticationService.softLogoutCredentials
// Listen for changes from deep links.
// Configure custom servers if already customised by a deep link.
let homeserver = authenticationService.state.homeserver.address
let identityServer = authenticationService.state.identityServer
if homeserver != BuildSettings.serverConfigDefaultHomeserverUrlString
|| (identityServer != nil && identityServer != BuildSettings.serverConfigDefaultIdentityServerUrlString) {
authenticationViewController.showCustomHomeserver(homeserver, andIdentityServer: identityServer)
}
// Listen for further changes from deep links.
AuthenticationService.shared.delegate = self
}
@@ -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
}
}
}
+24 -5
View File
@@ -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
+15 -10
View File
@@ -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)
+7 -3
View File
@@ -3683,7 +3683,8 @@ static CGSize kThreadListBarButtonItemImageSize;
}]];
}
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart)
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart &&
selectedEvent.eventType != MXEventTypeBeaconInfo)
{
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
@@ -3719,7 +3720,8 @@ static CGSize kThreadListBarButtonItemImageSize;
}]];
}
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart)
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart &&
selectedEvent.eventType != MXEventTypeBeaconInfo)
{
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
@@ -6769,7 +6771,7 @@ static CGSize kThreadListBarButtonItemImageSize;
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
BOOL result = (event.eventType != MXEventTypePollStart && (!attachment || attachment.type != MXKAttachmentTypeSticker));
BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker;
if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
{
@@ -6795,6 +6797,8 @@ static CGSize kThreadListBarButtonItemImageSize;
case MXEventTypeKeyVerificationMac:
case MXEventTypeKeyVerificationDone:
case MXEventTypeKeyVerificationCancel:
case MXEventTypePollStart:
case MXEventTypeBeaconInfo:
result = NO;
break;
case MXEventTypeCustom:
@@ -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
}
}
}
+1
View File
@@ -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)
}
}
@@ -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
}
@@ -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)
}
@@ -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
}
@@ -17,31 +17,10 @@
import XCTest
import RiotSwiftUI
class AuthenticationChoosePasswordUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationChoosePasswordScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationChoosePasswordUITests(selector: #selector(verifyAuthenticationChoosePasswordScreen))
}
func verifyAuthenticationChoosePasswordScreen() throws {
guard let screenState = screenState as? MockAuthenticationChoosePasswordScreenState else { fatalError("no screen") }
switch screenState {
case .emptyPassword:
verifyEmptyPassword()
case .enteredInvalidPassword:
verifyEnteredInvalidPassword()
case .enteredValidPassword:
verifyEnteredValidPassword()
case .enteredValidPasswordAndSignoutAllDevicesChecked:
verifyEnteredValidPasswordAndSignoutAllDevicesChecked()
}
}
func verifyEmptyPassword() {
class AuthenticationChoosePasswordUITests: MockScreenTestCase {
func testEmptyPassword() {
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.emptyPassword.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
@@ -58,7 +37,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
}
func verifyEnteredInvalidPassword() {
func testEnteredInvalidPassword() {
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredInvalidPassword.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
@@ -75,7 +56,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
}
func verifyEnteredValidPassword() {
func testEnteredValidPassword() {
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPassword.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
@@ -92,7 +75,9 @@ class AuthenticationChoosePasswordUITests: MockScreenTest {
XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked")
}
func verifyEnteredValidPasswordAndSignoutAllDevicesChecked() {
func testEnteredValidPasswordAndSignoutAllDevicesChecked() {
app.goToScreenWithIdentifier(MockAuthenticationChoosePasswordScreenState.enteredValidPasswordAndSignoutAllDevicesChecked.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown.")
@@ -20,8 +20,6 @@ import Foundation
struct AuthenticationHomeserverViewData: Equatable {
/// The homeserver string to be shown to the user.
let address: String
/// Whether or not the homeserver is matrix.org.
let isMatrixDotOrg: Bool
/// Whether or not to display the username and password text fields during login.
let showLoginForm: Bool
/// Whether or not to display the username and password text fields during registration.
@@ -36,7 +34,6 @@ extension AuthenticationHomeserverViewData {
/// A mock homeserver that is configured just like matrix.org.
static var mockMatrixDotOrg: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: "matrix.org",
isMatrixDotOrg: true,
showLoginForm: true,
showRegistrationForm: true,
ssoIdentityProviders: [
@@ -51,7 +48,6 @@ extension AuthenticationHomeserverViewData {
/// A mock homeserver that supports login and registration via a password but has no SSO providers.
static var mockBasicServer: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: "example.com",
isMatrixDotOrg: false,
showLoginForm: true,
showRegistrationForm: true,
ssoIdentityProviders: [])
@@ -60,7 +56,6 @@ extension AuthenticationHomeserverViewData {
/// A mock homeserver that supports only supports authentication via a single SSO provider.
static var mockEnterpriseSSO: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: "company.com",
isMatrixDotOrg: false,
showLoginForm: false,
showRegistrationForm: false,
ssoIdentityProviders: [SSOIdentityProvider(id: "test", name: "SAML", brand: nil, iconURL: nil)])
@@ -69,7 +64,6 @@ extension AuthenticationHomeserverViewData {
/// A mock homeserver that supports only supports authentication via fallback.
static var mockFallback: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: "company.com",
isMatrixDotOrg: false,
showLoginForm: false,
showRegistrationForm: false,
ssoIdentityProviders: [])
@@ -27,30 +27,25 @@ struct AuthenticationServerInfoSection: View {
// MARK: - Public
let address: String
let showMatrixDotOrgInfo: Bool
let flow: AuthenticationFlow
let editAction: () -> Void
var title: String {
flow == .login ? VectorL10n.authenticationServerInfoTitleLogin : VectorL10n.authenticationServerInfoTitle
}
// MARK: - Views
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(VectorL10n.authenticationServerInfoTitle)
Text(title)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.secondaryContent)
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(address)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
if showMatrixDotOrgInfo {
Text(VectorL10n.authenticationServerInfoMatrixDescription)
.font(theme.fonts.caption1)
.foregroundColor(theme.colors.tertiaryContent)
.accessibilityIdentifier("serverDescriptionText")
}
}
Text(address)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
Spacer()
@@ -174,13 +174,14 @@ class AuthenticationService: NSObject {
}
/// Reset the service to a fresh state.
func reset() {
/// - Parameter useDefaultServer: Pass `true` to revert back to the one in `BuildSettings`, otherwise the current homeserver will be kept.
func reset(useDefaultServer: Bool = false) {
loginWizard = nil
registrationWizard = nil
softLogoutCredentials = nil
// The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway.
let address = state.homeserver.addressFromUser ?? state.homeserver.address
let address = useDefaultServer ? BuildSettings.serverConfigDefaultHomeserverUrlString : state.homeserver.addressFromUser ?? state.homeserver.address
let identityServer = state.identityServer
self.state = AuthenticationState(flow: .login,
homeserverAddress: address,
@@ -196,27 +197,6 @@ class AuthenticationService: NSObject {
delegate?.authenticationService(self, didReceive: token, with: transactionID) ?? false
}
// /// Perform a well-known request, using the domain from the matrixId
// func getWellKnownData(matrixId: String,
// homeServerConnectionConfig: HomeServerConnectionConfig?) async -> WellknownResult {
//
// }
//
// /// Authenticate with a matrixId and a password
// /// Usually call this after a successful call to getWellKnownData()
// /// - Parameter homeServerConnectionConfig the information about the homeserver and other configuration
// /// - Parameter matrixId the matrixId of the user
// /// - Parameter password the password of the account
// /// - Parameter initialDeviceName the initial device name
// /// - Parameter deviceId the device id, optional. If not provided or null, the server will generate one.
// func directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
// matrixId: String,
// password: String,
// initialDeviceName: String,
// deviceId: String? = nil) async -> MXSession {
//
// }
// MARK: - Private
/// Query the supported login flows for the supplied homeserver.
@@ -65,7 +65,6 @@ struct AuthenticationState {
/// The homeserver mapped into view data that is ready for display.
var viewData: AuthenticationHomeserverViewData {
AuthenticationHomeserverViewData(address: displayableAddress,
isMatrixDotOrg: isMatrixDotOrg,
showLoginForm: preferredLoginMode.supportsPasswordFlow,
showRegistrationForm: registrationFlow != nil && !needsRegistrationFallback,
ssoIdentityProviders: preferredLoginMode.ssoIdentityProviders ?? [])
@@ -15,6 +15,7 @@
//
import Foundation
import libPhoneNumber_iOS
/// Set of methods to be able to login to an existing account on a homeserver.
///
@@ -42,11 +43,6 @@ class LoginWizard {
self.state = State()
}
// /// Get some information about a matrixId: displayName and avatar url
// func profileInfo(for matrixID: String) async -> LoginProfileInfo {
//
// }
/// Login to the homeserver.
/// - Parameters:
/// - login: The login field. Can be a user name, or a msisdn (email or phone number) associated to the account.
@@ -67,6 +63,13 @@ class LoginWizard {
password: password,
deviceDisplayName: initialDeviceName,
deviceID: deviceID)
} else if let number = try? NBPhoneNumberUtil.sharedInstance().parse(login, defaultRegion: nil),
NBPhoneNumberUtil.sharedInstance().isValidNumber(number) {
let msisdn = login.replacingOccurrences(of: "+", with: "")
parameters = LoginPasswordParameters(id: .thirdParty(medium: .msisdn, address: msisdn),
password: password,
deviceDisplayName: initialDeviceName,
deviceID: deviceID)
} else {
parameters = LoginPasswordParameters(id: .user(login),
password: password,
@@ -92,12 +95,6 @@ class LoginWizard {
client: client,
removeOtherAccounts: removeOtherAccounts)
}
// /// Login to the homeserver by sending a custom JsonDict.
// /// The data should contain at least one entry `type` with a String value.
// func loginCustom(data: Codable) async -> MXSession {
//
// }
/// Ask the homeserver to reset the user password. The password will not be
/// reset until `resetPasswordMailConfirmed` is successfully called.
@@ -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()
}
@@ -32,11 +32,18 @@ enum AuthenticationForgotPasswordViewModelResult {
// MARK: View
struct AuthenticationForgotPasswordViewState: BindableState {
/// The homeserver that the user is using to reset their password.
let homeserver: AuthenticationHomeserverViewData
/// An email has been sent and the app is waiting for the user to tap the link.
var hasSentEmail = false
/// View state that can be bound to from SwiftUI.
var bindings: AuthenticationForgotPasswordBindings
/// The message shown in the header while asking for an email address to be entered.
var formHeaderMessage: String {
VectorL10n.authenticationForgotPasswordInputMessage(homeserver.address)
}
/// Whether the email address is valid and the user can continue.
var hasInvalidAddress: Bool {
bindings.emailAddress.isEmpty
@@ -31,8 +31,9 @@ class AuthenticationForgotPasswordViewModel: AuthenticationForgotPasswordViewMod
// MARK: - Setup
init(emailAddress: String = "") {
let viewState = AuthenticationForgotPasswordViewState(bindings: AuthenticationForgotPasswordBindings(emailAddress: emailAddress))
init(homeserver: AuthenticationHomeserverViewData, emailAddress: String = "") {
let viewState = AuthenticationForgotPasswordViewState(homeserver: homeserver,
bindings: AuthenticationForgotPasswordBindings(emailAddress: emailAddress))
super.init(initialViewState: viewState)
}
@@ -20,6 +20,8 @@ import CommonKit
struct AuthenticationForgotPasswordCoordinatorParameters {
let navigationRouter: NavigationRouterType
let loginWizard: LoginWizard
/// The homeserver currently being used.
let homeserver: AuthenticationState.Homeserver
}
enum AuthenticationForgotPasswordCoordinatorResult {
@@ -63,7 +65,7 @@ final class AuthenticationForgotPasswordCoordinator: Coordinator, Presentable {
@MainActor init(parameters: AuthenticationForgotPasswordCoordinatorParameters) {
self.parameters = parameters
let viewModel = AuthenticationForgotPasswordViewModel()
let viewModel = AuthenticationForgotPasswordViewModel(homeserver: parameters.homeserver.viewData)
let view = AuthenticationForgotPasswordScreen(viewModel: viewModel.context)
authenticationForgotPasswordViewModel = viewModel
authenticationForgotPasswordHostingController = VectorHostingController(rootView: view)
@@ -37,11 +37,14 @@ enum MockAuthenticationForgotPasswordScreenState: MockScreenState, CaseIterable
let viewModel: AuthenticationForgotPasswordViewModel
switch self {
case .emptyAddress:
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "")
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "")
case .enteredAddress:
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "test@example.com")
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "test@example.com")
case .hasSentEmail:
viewModel = AuthenticationForgotPasswordViewModel(emailAddress: "test@example.com")
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "test@example.com")
Task { await viewModel.updateForSentEmail() }
}
@@ -17,35 +17,17 @@
import XCTest
import RiotSwiftUI
class AuthenticationForgotPasswordUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationForgotPasswordScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationForgotPasswordUITests(selector: #selector(verifyAuthenticationForgotPasswordScreen))
}
func verifyAuthenticationForgotPasswordScreen() throws {
guard let screenState = screenState as? MockAuthenticationForgotPasswordScreenState else { fatalError("no screen") }
switch screenState {
case .emptyAddress:
verifyEmptyAddress()
case .enteredAddress:
verifyEnteredAddress()
case .hasSentEmail:
verifyWaitingForEmailLink()
}
}
func verifyEmptyAddress() {
class AuthenticationForgotPasswordUITests: MockScreenTestCase {
func testEmptyAddress() {
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.emptyAddress.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
let addressTextField = app.textFields["addressTextField"]
XCTAssertTrue(addressTextField.exists, "The text field should be shown before an email is sent.")
XCTAssertEqual(addressTextField.value as? String, "Email Address", "The text field should be showing the placeholder before text is input.")
XCTAssertEqual(addressTextField.value as? String, VectorL10n.authenticationForgotPasswordTextFieldPlaceholder,
"The text field should be showing the placeholder before text is input.")
let nextButton = app.buttons["nextButton"]
XCTAssertTrue(nextButton.exists, "The next button should be shown before an email is sent.")
@@ -65,7 +47,9 @@ class AuthenticationForgotPasswordUITests: MockScreenTest {
XCTAssertEqual(cancelButton.label, "Cancel")
}
func verifyEnteredAddress() {
func testEnteredAddress() {
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.enteredAddress.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
@@ -91,7 +75,9 @@ class AuthenticationForgotPasswordUITests: MockScreenTest {
XCTAssertEqual(cancelButton.label, "Cancel")
}
func verifyWaitingForEmailLink() {
func testWaitingForEmailLink() {
app.goToScreenWithIdentifier(MockAuthenticationForgotPasswordScreenState.hasSentEmail.title)
XCTAssertFalse(app.staticTexts["titleLabel"].exists, "The title should be hidden once an email has been sent.")
XCTAssertFalse(app.staticTexts["messageLabel"].exists, "The message should be hidden once an email has been sent.")
XCTAssertFalse(app.textFields["addressTextField"].exists, "The text field should be hidden once an email has been sent.")
@@ -24,7 +24,7 @@ class AuthenticationForgotPasswordViewModelTests: XCTestCase {
var context: AuthenticationForgotPasswordViewModelType.Context!
override func setUpWithError() throws {
viewModel = AuthenticationForgotPasswordViewModel()
viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg)
context = viewModel.context
}
@@ -55,7 +55,7 @@ struct AuthenticationForgotPasswordForm: View {
.foregroundColor(theme.colors.primaryContent)
.accessibilityIdentifier("titleLabel")
Text(VectorL10n.authenticationForgotPasswordInputMessage)
Text(viewModel.viewState.formHeaderMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
@@ -195,8 +195,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
@MainActor private func parseUsername(_ username: String) {
guard MXTools.isMatrixUserIdentifier(username) else { return }
let domain = username.split(separator: ":")[1]
let homeserverAddress = HomeserverAddress.sanitized(String(domain))
let domain = username.components(separatedBy: ":")[1]
let homeserverAddress = HomeserverAddress.sanitized(domain)
startLoading(isInteractionBlocking: false)
@@ -260,7 +260,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable {
let modalRouter = NavigationRouter()
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
loginWizard: loginWizard)
loginWizard: loginWizard,
homeserver: parameters.authenticationService.state.homeserver)
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
@@ -17,57 +17,45 @@
import XCTest
import RiotSwiftUI
class AuthenticationLoginUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationLoginScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationLoginUITests(selector: #selector(verifyAuthenticationLoginScreen))
}
func verifyAuthenticationLoginScreen() throws {
guard let screenState = screenState as? MockAuthenticationLoginScreenState else { fatalError("no screen") }
switch screenState {
case .matrixDotOrg:
let state = "matrix.org"
validateServerDescriptionIsVisible(for: state)
validateLoginFormIsVisible(for: state)
validateSSOButtonsAreShown(for: state)
case .passwordOnly:
let state = "a password only server"
validateServerDescriptionIsHidden(for: state)
validateLoginFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateNextButtonIsDisabled(for: state)
case .passwordWithCredentials:
let state = "a password only server with credentials entered"
validateNextButtonIsEnabled(for: state)
case .ssoOnly:
let state = "an SSO only server"
validateServerDescriptionIsHidden(for: state)
validateLoginFormIsHidden(for: state)
validateSSOButtonsAreShown(for: state)
case .fallback:
let state = "a fallback server"
validateFallback(for: state)
}
}
/// Checks that the server description label is shown.
func validateServerDescriptionIsVisible(for state: String) {
let descriptionLabel = app.staticTexts["serverDescriptionText"]
class AuthenticationLoginUITests: MockScreenTestCase {
func testMatrixDotOrg() {
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.matrixDotOrg.title)
XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).")
XCTAssertEqual(descriptionLabel.label, VectorL10n.authenticationServerInfoMatrixDescription, "The server description should be correct for \(state).")
let state = "matrix.org"
validateLoginFormIsVisible(for: state)
validateSSOButtonsAreShown(for: state)
}
/// Checks that the server description label is hidden.
func validateServerDescriptionIsHidden(for state: String) {
let descriptionLabel = app.staticTexts["serverDescriptionText"]
XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).")
func testPasswordOnly() {
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordOnly.title)
let state = "a password only server"
validateLoginFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateNextButtonIsDisabled(for: state)
}
func testPasswordWithCredentials() {
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.passwordWithCredentials.title)
let state = "a password only server with credentials entered"
validateNextButtonIsEnabled(for: state)
}
func testSSOOnly() {
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.ssoOnly.title)
let state = "an SSO only server"
validateLoginFormIsHidden(for: state)
validateSSOButtonsAreShown(for: state)
}
func testFallback() {
app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.fallback.title)
let state = "a fallback server"
validateFallback(for: state)
}
/// Checks that the username and password text fields are shown along with the next button.
@@ -96,6 +96,7 @@ class AuthenticationLoginViewModelTests: XCTestCase {
context.username = "bob"
context.password = "12345678"
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be valid to submit.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
// When updating the view model whilst loading a homeserver.
@@ -103,12 +104,14 @@ class AuthenticationLoginViewModelTests: XCTestCase {
// Then the view state should reflect that the homeserver is loading.
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked from submission.")
// When updating the view model after loading a homeserver.
viewModel.update(isLoading: false)
// Then the view state should reflect that the homeserver is now loaded.
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should once again be valid to submit.")
}
@MainActor func testFallbackServer() {
@@ -37,15 +37,16 @@ struct AuthenticationLoginScreen: View {
VStack(spacing: 0) {
header
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
.padding(.bottom, 36)
.padding(.bottom, 28)
serverInfo
.padding(.leading, 12)
.padding(.bottom, 16)
Rectangle()
.fill(theme.colors.quinaryContent)
.frame(height: 1)
.padding(.vertical, 21)
.padding(.bottom, 22)
if viewModel.viewState.homeserver.showLoginForm {
loginForm
@@ -87,7 +88,7 @@ struct AuthenticationLoginScreen: View {
/// The sever information section that includes a button to select a different server.
var serverInfo: some View {
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address,
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
flow: .login) {
viewModel.send(viewAction: .selectServer)
}
}
@@ -104,8 +105,7 @@ struct AuthenticationLoginScreen: View {
onEditingChanged: usernameEditingChanged,
onCommit: { isPasswordFocused = true })
.accessibilityIdentifier("usernameTextField")
Spacer().frame(height: 20)
.padding(.bottom, 7)
RoundedBorderTextField(placeHolder: VectorL10n.authPasswordPlaceholder,
text: $viewModel.password,
@@ -17,6 +17,6 @@
import XCTest
import RiotSwiftUI
class AuthenticationReCaptchaUITests: MockScreenTest {
class AuthenticationReCaptchaUITests: MockScreenTestCase {
// Nothing to test as the view only has a single state.
}
@@ -62,15 +62,10 @@ struct AuthenticationReCaptchaScreen: View {
OnboardingIconImage(image: Asset.Images.onboardingCongratulationsIcon)
.padding(.bottom, 8)
Text(VectorL10n.authenticationRegistrationTitle)
Text(VectorL10n.authenticationRecaptchaTitle)
.font(theme.fonts.title2B)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.authenticationRecaptchaMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
}
}
@@ -92,7 +92,7 @@ struct AuthenticationRecaptchaWebView: UIViewRepresentable {
"""
<html>
<head>
<meta name='viewport' content='initial-scale=1.0' />
<meta name='viewport' content='initial-scale=1.0, user-scalable=no' />
<style>@media (prefers-color-scheme: dark) { body { background-color: #15191E; } }</style>
<script type="text/javascript">
var verifyCallback = function(response) {
@@ -50,8 +50,19 @@ enum AuthenticationRegistrationViewModelResult: CustomStringConvertible {
// MARK: View
struct AuthenticationRegistrationViewState: BindableState {
enum UsernameAvailability {
/// The availability of the username is unknown.
case unknown
/// The username is available.
case available
/// The username is invalid for the following reason.
case invalid(String)
}
/// Data about the selected homeserver.
var homeserver: AuthenticationHomeserverViewData
/// Whether a new homeserver is currently being loaded.
var isLoading: Bool = false
/// View state that can be bound to from SwiftUI.
var bindings: AuthenticationRegistrationBindings
/// Whether or not the username field has been edited yet.
@@ -63,12 +74,22 @@ struct AuthenticationRegistrationViewState: BindableState {
/// This is used to delay showing an error state until the user has tried 1 password.
var hasEditedPassword = false
/// An error message to be shown in the username text field footer.
var usernameErrorMessage: String?
/// The availability of the currently enetered username.
var usernameAvailability: UsernameAvailability = .unknown
/// The message to show in the username text field footer.
var usernameFooterMessage: String {
usernameErrorMessage ?? VectorL10n.authenticationRegistrationUsernameFooter
switch usernameAvailability {
case .unknown:
return VectorL10n.authenticationRegistrationUsernameFooter
case .invalid(let errorMessage):
return errorMessage
case .available:
// https is never shown to the user but http is, so strip the scheme.
let domain = homeserver.address.replacingOccurrences(of: "http://", with: "")
let userID = "@\(bindings.username):\(domain)"
return VectorL10n.authenticationRegistrationUsernameFooterAvailable(userID)
}
}
/// Whether to show any SSO buttons.
@@ -76,19 +97,28 @@ struct AuthenticationRegistrationViewState: BindableState {
!homeserver.ssoIdentityProviders.isEmpty
}
/// Whether the current `username` is valid.
var isUsernameValid: Bool {
!bindings.username.isEmpty && usernameErrorMessage == nil
/// Whether the current `username` is invalid.
var isUsernameInvalid: Bool {
if case .invalid = usernameAvailability {
return true
} else {
return bindings.username.isEmpty
}
}
/// Whether the current `password` is valid.
var isPasswordValid: Bool {
bindings.password.count >= 8
/// Whether the current `password` is invalid.
var isPasswordInvalid: Bool {
bindings.password.count < 8
}
/// `true` if it is possible to continue, otherwise `false`.
var hasValidCredentials: Bool {
isUsernameValid && isPasswordValid
!isUsernameInvalid && !isPasswordInvalid
}
/// `true` if valid credentials have been entered and the homeserver is loaded.
var canSubmit: Bool {
hasValidCredentials && !isLoading
}
}
@@ -108,8 +138,8 @@ enum AuthenticationRegistrationViewAction {
case validateUsername
/// Allows password validation to take place (sent after editing the password for the first time).
case enablePasswordValidation
/// Clear any errors being shown in the username text field footer.
case clearUsernameError
/// Clear any availability messages being shown in the username text field footer.
case resetUsernameAvailability
/// Continue using the input username and password.
case next
/// Continue using the supplied SSO provider.
@@ -48,8 +48,8 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
Task { await validateUsername() }
case .enablePasswordValidation:
Task { await enablePasswordValidation() }
case .clearUsernameError:
Task { await clearUsernameError() }
case .resetUsernameAvailability:
Task { await resetUsernameAvailability() }
case .next:
Task { await callback?(.createAccount(username: state.bindings.username, password: state.bindings.password)) }
case .continueWithSSO(let provider):
@@ -59,14 +59,29 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
}
}
@MainActor func update(isLoading: Bool) {
guard state.isLoading != isLoading else { return }
state.isLoading = isLoading
}
@MainActor func update(homeserver: AuthenticationHomeserverViewData) {
state.homeserver = homeserver
}
@MainActor func update(username: String) {
guard username != state.bindings.username else { return }
state.bindings.username = username
}
@MainActor func confirmUsernameAvailability(_ username: String) {
guard username == state.bindings.username else { return }
state.usernameAvailability = .available
}
@MainActor func displayError(_ type: AuthenticationRegistrationErrorType) {
switch type {
case .usernameUnavailable(let message):
state.usernameErrorMessage = message
state.usernameAvailability = .invalid(message)
case .mxError(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: VectorL10n.error,
@@ -101,9 +116,9 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
state.hasEditedPassword = true
}
/// Clear any errors being shown in the username text field footer.
@MainActor private func clearUsernameError() {
guard state.usernameErrorMessage != nil else { return }
state.usernameErrorMessage = nil
/// Reset the username's availability, clearing any messages being shown in the username text field footer.
@MainActor private func resetUsernameAvailability() {
if case .unknown = state.usernameAvailability { return }
state.usernameAvailability = .unknown
}
}
@@ -21,10 +21,22 @@ protocol AuthenticationRegistrationViewModelProtocol {
var callback: (@MainActor (AuthenticationRegistrationViewModelResult) -> Void)? { get set }
var context: AuthenticationRegistrationViewModelType.Context { get }
/// Update the view to reflect that a new homeserver is being loaded.
/// - Parameter isLoading: Whether or not the homeserver is being loaded.
@MainActor func update(isLoading: Bool)
/// Update the view with new homeserver information.
/// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`.
@MainActor func update(homeserver: AuthenticationHomeserverViewData)
/// Update the username, for example to convert a full MXID into just the local part.
/// - Parameter username: The username to be shown instead.
@MainActor func update(username: String)
/// Update the view to confirm that the chosen username is available.
/// - Parameter username: The username that was checked.
@MainActor func confirmUsernameAvailability(_ username: String)
/// Display an error to the user.
/// - Parameter type: The type of error to be displayed.
@MainActor func displayError(_ type: AuthenticationRegistrationErrorType)
@@ -129,18 +129,58 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
}
}
/// Show a blocking activity indicator whilst saving.
@MainActor private func startLoading() {
waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
/// Show an activity indicator whilst loading.
/// - Parameter isInteractionBlocking: Whether or not the indicator blocks user interaction.
@MainActor private func startLoading(isInteractionBlocking: Bool = true) {
waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: isInteractionBlocking))
if !isInteractionBlocking {
authenticationRegistrationViewModel.update(isLoading: true)
}
}
/// Hide the currently displayed activity indicator.
@MainActor private func stopLoading() {
authenticationRegistrationViewModel.update(isLoading: false)
waitingIndicator = nil
}
/// Asks the homeserver to check the supplied username's format and availability.
/// Updates the homeserver if a full MXID is entered, then requests whether the username is valid and available.
@MainActor private func validateUsername(_ username: String) {
guard MXTools.isMatrixUserIdentifier(username) else {
// Continue with availability check for a normal username.
confirmAvailability(of: username)
return
}
// Otherwise split out the domain and username and update the homeserver first.
let components = username.dropFirst().components(separatedBy: ":")
let domain = components[1]
let username = components[0]
let homeserverAddress = HomeserverAddress.sanitized(domain)
startLoading(isInteractionBlocking: false)
currentTask = Task { [weak self] in
do {
try await authenticationService.startFlow(.register, for: homeserverAddress)
guard !Task.isCancelled else { return }
self?.updateViewModelHomeserver()
self?.authenticationRegistrationViewModel.update(username: username)
self?.stopLoading()
self?.confirmAvailability(of: username)
} catch {
self?.stopLoading()
self?.handleError(error)
}
}
}
/// Asks the homeserver to check the supplied username's format and availability.
@MainActor private func confirmAvailability(of username: String) {
guard let registrationWizard = registrationWizard else {
MXLog.failure("[AuthenticationRegistrationCoordinator] The registration wizard was requested before getting the login flow.")
return
@@ -149,6 +189,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
currentTask = Task {
do {
_ = try await registrationWizard.registrationAvailable(username: username)
authenticationRegistrationViewModel.confirmUsernameAvailability(username)
} catch {
guard !Task.isCancelled, let mxError = MXError(nsError: error as NSError) else { return }
if mxError.errcode == kMXErrCodeStringUserInUse
@@ -244,12 +285,16 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
@MainActor private func serverSelectionCoordinator(_ coordinator: AuthenticationServerSelectionCoordinator,
didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) {
if result == .updated {
let homeserver = authenticationService.state.homeserver
authenticationRegistrationViewModel.update(homeserver: homeserver.viewData)
updateViewModelHomeserver()
}
navigationRouter.dismissModule(animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
@MainActor private func updateViewModelHomeserver() {
let homeserver = authenticationService.state.homeserver
authenticationRegistrationViewModel.update(homeserver: homeserver.viewData)
}
}
@@ -47,6 +47,7 @@ enum MockAuthenticationRegistrationScreenState: MockScreenState, CaseIterable {
viewModel = AuthenticationRegistrationViewModel(homeserver: .mockBasicServer)
viewModel.context.username = "alice"
viewModel.context.password = "password"
Task { await viewModel.confirmUsernameAvailability("alice") }
case .passwordWithUsernameError:
viewModel = AuthenticationRegistrationViewModel(homeserver: .mockBasicServer)
viewModel.state.hasEditedUsername = true
@@ -17,66 +17,78 @@
import XCTest
import RiotSwiftUI
class AuthenticationRegistrationUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationRegistrationScreenState.self
class AuthenticationRegistrationUITests: MockScreenTestCase {
func testMatrixDotOrg() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.matrixDotOrg.title)
let state = "matrix.org"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreShown(for: state)
validateFallbackButtonIsHidden(for: state)
validateUnknownUsernameAvailability(for: state)
validateNoPasswordErrorsAreShown(for: state)
}
override class func createTest() -> MockScreenTest {
return AuthenticationRegistrationUITests(selector: #selector(verifyAuthenticationRegistrationScreen))
func testPasswordOnly() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordOnly.title)
let state = "a password only server"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsDisabled(for: state)
validateUnknownUsernameAvailability(for: state)
validateNoPasswordErrorsAreShown(for: state)
}
func verifyAuthenticationRegistrationScreen() throws {
guard let screenState = screenState as? MockAuthenticationRegistrationScreenState else { fatalError("no screen") }
switch screenState {
case .matrixDotOrg:
let state = "matrix.org"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreShown(for: state)
validateFallbackButtonIsHidden(for: state)
validateNoErrorsAreShown(for: state)
case .passwordOnly:
let state = "a password only server"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsDisabled(for: state)
validateNoErrorsAreShown(for: state)
case .passwordWithCredentials:
let state = "a password only server with credentials entered"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsEnabled(for: state)
validateNoErrorsAreShown(for: state)
case .passwordWithUsernameError:
let state = "a password only server with an invalid username"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsDisabled(for: state)
validateUsernameError(for: state)
case .ssoOnly:
let state = "an SSO only server"
validateRegistrationFormIsHidden(for: state)
validateSSOButtonsAreShown(for: state)
validateFallbackButtonIsHidden(for: state)
case .fallback:
let state = "fallback"
validateRegistrationFormIsHidden(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsShown(for: state)
}
func testPasswordWithCredentials() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordWithCredentials.title)
let state = "a password only server with credentials entered"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsEnabled(for: state)
validateUsernameAvailable(for: state)
validateNoPasswordErrorsAreShown(for: state)
}
func testPasswordWithUsernameError() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.passwordWithUsernameError.title)
let state = "a password only server with an invalid username"
validateRegistrationFormIsVisible(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsHidden(for: state)
validateNextButtonIsDisabled(for: state)
validateUsernameError(for: state)
}
func testSSOOnly() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.ssoOnly.title)
let state = "an SSO only server"
validateRegistrationFormIsHidden(for: state)
validateSSOButtonsAreShown(for: state)
validateFallbackButtonIsHidden(for: state)
}
func testFallback() {
app.goToScreenWithIdentifier(MockAuthenticationRegistrationScreenState.fallback.title)
let state = "fallback"
validateRegistrationFormIsHidden(for: state)
validateSSOButtonsAreHidden(for: state)
validateFallbackButtonIsShown(for: state)
}
/// Checks that the username and password text fields are shown along with the next button.
func validateRegistrationFormIsVisible(for state: String) {
let usernameTextField = app.textFields.element
@@ -147,15 +159,24 @@ class AuthenticationRegistrationUITests: MockScreenTest {
XCTAssertEqual(usernameFooter.label, VectorL10n.authInvalidUserName, "The username footer should be showing an error for \(state).")
}
/// Checks that neither the username or password text field footers are showing an error.
func validateNoErrorsAreShown(for state: String) {
func validateUsernameAvailable(for state: String) {
let usernameFooter = textFieldFooter(for: "usernameTextField")
XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).")
XCTAssertTrue(usernameFooter.label.starts(with: VectorL10n.authenticationRegistrationUsernameFooterAvailable("")),
"The username footer should be showing the username as available for \(state).")
}
func validateUnknownUsernameAvailability(for state: String) {
let usernameFooter = textFieldFooter(for: "usernameTextField")
let passwordFooter = textFieldFooter(for: "passwordTextField")
XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).")
XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).")
XCTAssertEqual(usernameFooter.label, VectorL10n.authenticationRegistrationUsernameFooter,
"The username footer should be showing the default message for \(state).")
}
/// Checks that neither the username or password text field footers are showing an error.
func validateNoPasswordErrorsAreShown(for state: String) {
let passwordFooter = textFieldFooter(for: "passwordTextField")
XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).")
XCTAssertEqual(passwordFooter.label, VectorL10n.authenticationRegistrationPasswordFooter,
"The password footer should be showing the default message for \(state).")
}
@@ -63,40 +63,89 @@ import Combine
}
func testUsernameError() async throws {
// Given a form with a valid username.
// Given a form with an entered username.
context.username = "bob"
XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error when the view model is created.")
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid if there is no error.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
// When displaying the error as a username error.
let errorMessage = "Username unavailable"
viewModel.displayError(.usernameUnavailable(errorMessage))
// Then the error should be shown in the footer.
XCTAssertEqual(context.viewState.usernameErrorMessage, errorMessage, "The error message should be stored.")
guard case let .invalid(displayedError) = context.viewState.usernameAvailability else {
XCTFail("The username should be invalid when an error is shown.")
return
}
XCTAssertEqual(displayedError, errorMessage, "The error message should match.")
XCTAssertEqual(context.viewState.usernameFooterMessage, errorMessage, "The error message should replace the standard footer message.")
XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.")
// When clearing the error.
context.send(viewAction: .clearUsernameError)
context.send(viewAction: .resetUsernameAvailability)
// Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors.
try await Task.sleep(nanoseconds: 100_000_000)
await Task.yield()
// Then the error should be hidden again.
XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error anymore.")
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.")
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when an error is cleared.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.")
}
func testUsernameAvailability() async throws {
// Given a form with an entered username.
context.username = "bob"
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
// When updating the state for an available username
viewModel.confirmUsernameAvailability("bob")
// Then the error should be shown in the footer.
XCTAssertEqual(context.viewState.usernameAvailability, .available,
"The username should be detected as available.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooterAvailable("@bob:matrix.org"),
"The footer message should display that the username is available.")
XCTAssertFalse(context.viewState.isUsernameInvalid,
"The username should continue to be valid when it is available.")
// When clearing the error.
context.send(viewAction: .resetUsernameAvailability)
// Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors.
await Task.yield()
// Then the error should be hidden again.
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.")
}
func testUsernameAvailabilityWhenChanged() async throws {
// Given a form with an entered username.
context.username = "robert"
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.")
// When updating the state for an available username that was previously entered.
viewModel.confirmUsernameAvailability("bob")
// Then the username should not be shown as available.
XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should not be updated.")
XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should continue to be valid when unverified.")
}
func testEmptyUsernameWithShortPassword() {
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
// When entering a password of 7 characters without a username.
@@ -104,8 +153,8 @@ import Combine
context.password = "1234567"
// Then the credentials should remain invalid.
XCTAssertFalse(context.viewState.isPasswordValid, "A 7-character password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "A 7-character password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
}
@@ -113,8 +162,8 @@ import Combine
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
// When entering a password of 8 characters without a username.
@@ -122,8 +171,8 @@ import Combine
context.password = "12345678"
// Then the password should be valid but the credentials should still be invalid.
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
}
@@ -131,8 +180,8 @@ import Combine
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
// When entering a username without a password.
@@ -140,8 +189,8 @@ import Combine
context.password = ""
// Then the username should be valid but the credentials should still be invalid.
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
}
@@ -149,8 +198,8 @@ import Combine
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
// When entering a username and password and encountering a username error
@@ -161,8 +210,8 @@ import Combine
viewModel.displayError(.usernameUnavailable(errorMessage))
// Then the password should be valid but the credentials should still be invalid.
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.")
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
}
@@ -170,8 +219,8 @@ import Combine
// Given a form with an empty username and password.
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.")
XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.")
XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.")
XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.")
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
// When entering a username and an 8-character password.
@@ -179,8 +228,63 @@ import Combine
context.password = "12345678"
// Then the credentials should be considered valid.
XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.")
XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.")
XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.")
XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.")
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
}
@MainActor func testLoadingServer() {
// Given a form with valid credentials.
context.username = "bob"
context.password = "12345678"
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
XCTAssertTrue(context.viewState.canSubmit, "The form should be valid to submit.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
// When updating the view model whilst loading a homeserver.
viewModel.update(isLoading: true)
// Then the view state should reflect that the homeserver is loading.
XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.")
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked from submission.")
// When updating the view model after loading a homeserver.
viewModel.update(isLoading: false)
// Then the view state should reflect that the homeserver is now loaded.
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
XCTAssertTrue(context.viewState.canSubmit, "The form should once again be valid to submit.")
}
@MainActor func testUpdatingUsername() {
// Given a form with valid credentials.
let fullMXID = "@bob:example.com"
context.username = fullMXID
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid without a password.")
XCTAssertFalse(context.viewState.canSubmit, "The form not be ready to submit without a password.")
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
// When updating the view model with a new username.
let localPart = "bob"
viewModel.update(username: localPart)
// Then the view state should reflect that the homeserver is loading.
XCTAssertEqual(context.username, localPart, "The username should match the value passed to the update method.")
}
}
extension AuthenticationRegistrationViewState.UsernameAvailability: Equatable {
public static func == (lhs: AuthenticationRegistrationViewState.UsernameAvailability,
rhs: AuthenticationRegistrationViewState.UsernameAvailability) -> Bool {
switch (lhs, rhs) {
case (.unknown, .unknown):
return true
case (.available, .available):
return true
case (.invalid, .invalid):
return true
default:
return false
}
}
}
@@ -35,15 +35,16 @@ struct AuthenticationRegistrationScreen: View {
VStack(spacing: 0) {
header
.padding(.top, OnboardingMetrics.topPaddingToNavigationBar)
.padding(.bottom, 36)
.padding(.bottom, 28)
serverInfo
.padding(.leading, 12)
.padding(.bottom, 16)
Rectangle()
.fill(theme.colors.quinaryContent)
.frame(height: 1)
.padding(.vertical, 21)
.padding(.bottom, 22)
if viewModel.viewState.homeserver.showRegistrationForm {
registrationForm
@@ -84,18 +85,13 @@ struct AuthenticationRegistrationScreen: View {
.font(theme.fonts.title2B)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.authenticationRegistrationMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
}
}
/// The sever information section that includes a button to select a different server.
var serverInfo: some View {
AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address,
showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) {
flow: .register) {
viewModel.send(viewAction: .selectServer)
}
}
@@ -107,21 +103,21 @@ struct AuthenticationRegistrationScreen: View {
placeHolder: VectorL10n.authenticationRegistrationUsername,
text: $viewModel.username,
footerText: viewModel.viewState.usernameFooterMessage,
isError: viewModel.viewState.hasEditedUsername && !viewModel.viewState.isUsernameValid,
isError: viewModel.viewState.hasEditedUsername && viewModel.viewState.isUsernameInvalid,
isFirstResponder: false,
configuration: UIKitTextInputConfiguration(returnKeyType: .next,
autocapitalizationType: .none,
autocorrectionType: .no),
onEditingChanged: usernameEditingChanged,
onCommit: { isPasswordFocused = true })
.onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .clearUsernameError) }
.onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .resetUsernameAvailability) }
.accessibilityIdentifier("usernameTextField")
RoundedBorderTextField(title: nil,
placeHolder: VectorL10n.authPasswordPlaceholder,
text: $viewModel.password,
footerText: VectorL10n.authenticationRegistrationPasswordFooter,
isError: viewModel.viewState.hasEditedPassword && !viewModel.viewState.isPasswordValid,
isError: viewModel.viewState.hasEditedPassword && viewModel.viewState.isPasswordInvalid,
isFirstResponder: isPasswordFocused,
configuration: UIKitTextInputConfiguration(returnKeyType: .done,
isSecureTextEntry: true),
@@ -133,7 +129,7 @@ struct AuthenticationRegistrationScreen: View {
Text(VectorL10n.next)
}
.buttonStyle(PrimaryActionButtonStyle())
.disabled(!viewModel.viewState.hasValidCredentials)
.disabled(!viewModel.viewState.canSubmit)
.accessibilityIdentifier("nextButton")
}
}
@@ -177,7 +173,7 @@ struct AuthenticationRegistrationScreen: View {
/// Sends the `next` view action so long as valid credentials have been input.
func submit() {
guard viewModel.viewState.hasValidCredentials else { return }
guard viewModel.viewState.canSubmit else { return }
viewModel.send(viewAction: .next)
}
@@ -32,12 +32,17 @@ struct AuthenticationServerSelectionViewState: BindableState {
var bindings: AuthenticationServerSelectionBindings
/// An error message to be shown in the text field footer.
var footerErrorMessage: String?
/// The flow that the screen is being used for.
let flow: AuthenticationFlow
/// Whether the screen is presented modally or within a navigation stack.
var hasModalPresentation: Bool
/// The message to show in the text field footer.
var footerMessage: String {
footerErrorMessage ?? VectorL10n.authenticationServerSelectionServerFooter
var headerTitle: String {
flow == .login ? VectorL10n.authenticationServerSelectionLoginTitle : VectorL10n.authenticationServerSelectionRegisterTitle
}
var headerMessage: String {
flow == .login ? VectorL10n.authenticationServerSelectionLoginMessage : VectorL10n.authenticationServerSelectionRegisterMessage
}
/// The title shown on the confirm button.
@@ -32,9 +32,10 @@ class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewM
// MARK: - Setup
init(homeserverAddress: String, hasModalPresentation: Bool) {
init(homeserverAddress: String, flow: AuthenticationFlow, hasModalPresentation: Bool) {
let bindings = AuthenticationServerSelectionBindings(homeserverAddress: homeserverAddress)
super.init(initialViewState: AuthenticationServerSelectionViewState(bindings: bindings,
flow: flow,
hasModalPresentation: hasModalPresentation))
}
@@ -59,6 +59,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
let homeserver = parameters.authenticationService.state.homeserver
let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.displayableAddress,
flow: parameters.authenticationService.state.flow,
hasModalPresentation: parameters.hasModalPresentation)
let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context)
authenticationServerSelectionViewModel = viewModel
@@ -26,6 +26,7 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
case matrix
case emptyAddress
case invalidAddress
case login
case nonModal
/// The associated screen
@@ -39,16 +40,24 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
switch self {
case .matrix:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
flow: .register,
hasModalPresentation: true)
case .emptyAddress:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "",
flow: .register,
hasModalPresentation: true)
case .invalidAddress:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "thisisbad",
flow: .register,
hasModalPresentation: true)
Task { await viewModel.displayError(.footerMessage(VectorL10n.errorCommonMessage)) }
case .login:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
flow: .login,
hasModalPresentation: true)
case .nonModal:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
flow: .register,
hasModalPresentation: false)
}
@@ -17,31 +17,15 @@
import XCTest
import RiotSwiftUI
class AuthenticationServerSelectionUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationServerSelectionScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationServerSelectionUITests(selector: #selector(verifyAuthenticationServerSelectionScreen))
}
func verifyAuthenticationServerSelectionScreen() throws {
guard let screenState = screenState as? MockAuthenticationServerSelectionScreenState else { fatalError("no screen") }
switch screenState {
case .matrix:
verifyNormalState()
case .emptyAddress:
verifyEmptyAddress()
case .invalidAddress:
verifyInvalidAddress()
case .nonModal:
verifyNonModalPresentation()
}
}
func verifyNormalState() {
class AuthenticationServerSelectionUITests: MockScreenTestCase {
func testRegisterState() {
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.matrix.title)
let title = app.staticTexts["headerTitle"]
XCTAssertEqual(title.label, VectorL10n.authenticationServerSelectionRegisterTitle)
let message = app.staticTexts["headerMessage"]
XCTAssertEqual(message.label, VectorL10n.authenticationServerSelectionRegisterMessage)
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org as passed to the view model init.")
@@ -51,14 +35,25 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
XCTAssertTrue(confirmButton.isEnabled, "The confirm button should be enabled when there is an address.")
let textFieldFooter = app.staticTexts["textFieldFooter"]
XCTAssertTrue(textFieldFooter.exists)
XCTAssertEqual(textFieldFooter.label, VectorL10n.authenticationServerSelectionServerFooter)
XCTAssertFalse(textFieldFooter.exists, "The footer shouldn't be shown when there isn't an error.")
let dismissButton = app.buttons["dismissButton"]
XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.")
}
func verifyEmptyAddress() {
func testLoginState() {
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.login.title)
let title = app.staticTexts["headerTitle"]
XCTAssertEqual(title.label, VectorL10n.authenticationServerSelectionLoginTitle)
let message = app.staticTexts["headerMessage"]
XCTAssertEqual(message.label, VectorL10n.authenticationServerSelectionLoginMessage)
}
func testEmptyAddress() {
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.emptyAddress.title)
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, VectorL10n.authenticationServerSelectionServerUrl, "The text field should show placeholder text in this state.")
@@ -67,7 +62,9 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when the address is empty.")
}
func verifyInvalidAddress() {
func testInvalidAddress() {
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.invalidAddress.title)
let serverTextField = app.textFields.element
XCTAssertEqual(serverTextField.value as? String, "thisisbad", "The text field should show the entered server.")
@@ -80,7 +77,9 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
XCTAssertEqual(textFieldFooter.label, VectorL10n.errorCommonMessage)
}
func verifyNonModalPresentation() {
func testNonModalPresentation() {
app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.nonModal.title)
let dismissButton = app.buttons["dismissButton"]
XCTAssertFalse(dismissButton.exists, "The dismiss button should be hidden when not in modal presentation.")
@@ -27,14 +27,14 @@ class AuthenticationServerSelectionViewModelTests: XCTestCase {
var context: AuthenticationServerSelectionViewModelType.Context!
override func setUp() {
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "", hasModalPresentation: true)
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "", flow: .login, hasModalPresentation: true)
context = viewModel.context
}
@MainActor func testErrorMessage() async throws {
// Given a new instance of the view model.
XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.")
XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown.")
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error shown.")
// When an error occurs.
let message = "Unable to contact server."
@@ -42,16 +42,16 @@ class AuthenticationServerSelectionViewModelTests: XCTestCase {
// Then the footer should now be showing an error.
XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.")
XCTAssertEqual(context.viewState.footerMessage, message, "The error message should be shown.")
XCTAssertTrue(context.viewState.isShowingFooterError, "There should be an error shown.")
// And when clearing the error.
context.send(viewAction: .clearFooterError)
// Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors.
try await Task.sleep(nanoseconds: 100_000_000)
await Task.yield()
// Then the error message should now be removed.
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown again.")
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error shown anymore.")
}
}
@@ -62,15 +62,17 @@ struct AuthenticationServerSelectionScreen: View {
OnboardingIconImage(image: Asset.Images.authenticationServerSelectionIcon)
.padding(.bottom, 8)
Text(VectorL10n.authenticationServerSelectionTitle)
Text(viewModel.viewState.headerTitle)
.font(theme.fonts.title2B)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
.accessibilityIdentifier("headerTitle")
Text(VectorL10n.authenticationServerSelectionMessage)
Text(viewModel.viewState.headerMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
.accessibilityIdentifier("headerMessage")
}
}
@@ -85,11 +87,13 @@ struct AuthenticationServerSelectionScreen: View {
textField
}
Text(viewModel.viewState.footerMessage)
.font(theme.fonts.footnote)
.foregroundColor(textFieldFooterColor)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityIdentifier("textFieldFooter")
if let errorMessage = viewModel.viewState.footerErrorMessage {
Text(errorMessage)
.font(theme.fonts.footnote)
.foregroundColor(textFieldFooterColor)
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityIdentifier("textFieldFooter")
}
}
Button(action: submit) {
@@ -155,7 +155,8 @@ final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable {
let modalRouter = NavigationRouter()
let parameters = AuthenticationForgotPasswordCoordinatorParameters(navigationRouter: modalRouter,
loginWizard: loginWizard)
loginWizard: loginWizard,
homeserver: parameters.authenticationService.state.homeserver)
let coordinator = AuthenticationForgotPasswordCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
@@ -17,35 +17,10 @@
import XCTest
import RiotSwiftUI
class AuthenticationSoftLogoutUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationSoftLogoutScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationSoftLogoutUITests(selector: #selector(verifyAuthenticationSoftLogoutScreen))
}
func verifyAuthenticationSoftLogoutScreen() throws {
guard let screenState = screenState as? MockAuthenticationSoftLogoutScreenState else { fatalError("no screen") }
switch screenState {
case .emptyPassword:
verifyEmptyPassword()
case .enteredPassword:
verifyEnteredPassword()
case .ssoOnly:
verifySSOOnly()
case .noSSO:
verifyNoSSO()
case .fallback:
verifyFallback()
case .noKeyBackup:
verifyNoKeyBackup()
}
}
func verifyEmptyPassword() {
class AuthenticationSoftLogoutUITests: MockScreenTestCase {
func testEmptyPassword() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.emptyPassword.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
@@ -77,7 +52,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
}
func verifyEnteredPassword() {
func testEnteredPassword() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.enteredPassword.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
@@ -109,7 +86,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
}
func verifySSOOnly() {
func testSSOOnly() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.ssoOnly.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
@@ -138,7 +117,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown.")
}
func verifyNoSSO() {
func testNoSSO() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noSSO.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
@@ -167,7 +148,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
}
func verifyFallback() {
func testFallback() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.fallback.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel1"].exists, "The message 1 should be shown.")
XCTAssertTrue(app.staticTexts["messageLabel2"].exists, "The message 2 should be shown.")
@@ -197,7 +180,9 @@ class AuthenticationSoftLogoutUITests: MockScreenTest {
XCTAssertEqual(ssoButtons.count, 0, "There should be no SSO button shown.")
}
func verifyNoKeyBackup() {
func testNoKeyBackup() {
app.goToScreenWithIdentifier(MockAuthenticationSoftLogoutScreenState.noKeyBackup.title)
XCTAssertFalse(app.staticTexts["messageLabel2"].exists, "The message 2 should not be shown.")
}
@@ -44,9 +44,15 @@ enum AuthenticationTermsViewModelResult {
// MARK: View
struct AuthenticationTermsViewState: BindableState {
/// The homeserver asking the user to accept the terms.
let homeserver: AuthenticationHomeserverViewData
/// View state that can be bound to from SwiftUI.
var bindings: AuthenticationTermsBindings
var headerMessage: String {
VectorL10n.authenticationTermsMessage(homeserver.address)
}
/// Whether or not all of the policies have been accepted.
var hasAcceptedAllPolicies: Bool {
bindings.policies.allSatisfy(\.accepted)
@@ -33,8 +33,9 @@ class AuthenticationTermsViewModel: AuthenticationTermsViewModelType, Authentica
// MARK: - Setup
init(policies: [AuthenticationTermsPolicy]) {
super.init(initialViewState: AuthenticationTermsViewState(bindings: AuthenticationTermsBindings(policies: policies)))
init(homeserver: AuthenticationHomeserverViewData, policies: [AuthenticationTermsPolicy]) {
super.init(initialViewState: AuthenticationTermsViewState(homeserver: homeserver,
bindings: AuthenticationTermsBindings(policies: policies)))
}
// MARK: - Public
@@ -22,8 +22,8 @@ struct AuthenticationTermsCoordinatorParameters {
let registrationWizard: RegistrationWizard
/// The policies to be accepted by the user.
let localizedPolicies: [MXLoginPolicyData]
/// The address of the homeserver (shown beneath the policies).
let homeserverAddress: String
/// The homeserver that provided the policies.
let homeserver: AuthenticationState.Homeserver
}
final class AuthenticationTermsCoordinator: Coordinator, Presentable {
@@ -59,10 +59,10 @@ final class AuthenticationTermsCoordinator: Coordinator, Presentable {
@MainActor init(parameters: AuthenticationTermsCoordinatorParameters) {
self.parameters = parameters
let subtitle = parameters.homeserverAddress
let subtitle = parameters.homeserver.displayableAddress
let policies = parameters.localizedPolicies.compactMap { AuthenticationTermsPolicy(url: $0.url, title: $0.name, subtitle: subtitle) }
let viewModel = AuthenticationTermsViewModel(policies: policies)
let viewModel = AuthenticationTermsViewModel(homeserver: parameters.homeserver.viewData, policies: policies)
let view = AuthenticationTermsScreen(viewModel: viewModel.context)
authenticationTermsViewModel = viewModel
authenticationTermsHostingController = VectorHostingController(rootView: view)
@@ -37,16 +37,18 @@ enum MockAuthenticationTermsScreenState: MockScreenState, CaseIterable {
let viewModel: AuthenticationTermsViewModel
switch self {
case .matrixDotOrg:
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
title: "Terms and Conditions",
subtitle: "matrix.org")])
case .accepted:
viewModel = AuthenticationTermsViewModel(policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
viewModel = AuthenticationTermsViewModel(homeserver: .mockMatrixDotOrg,
policies: [AuthenticationTermsPolicy(url: "https://matrix-client.matrix.org/_matrix/consent?v=1.0",
title: "Terms and Conditions",
subtitle: "matrix.org",
accepted: true)])
case .multiple:
viewModel = AuthenticationTermsViewModel(policies: [
viewModel = AuthenticationTermsViewModel(homeserver: .mockBasicServer, policies: [
AuthenticationTermsPolicy(url: "https://example.com/terms", title: "Terms and Conditions", subtitle: "example.com"),
AuthenticationTermsPolicy(url: "https://example.com/privacy", title: "Privacy Policy", subtitle: "example.com"),
AuthenticationTermsPolicy(url: "https://example.com/conduct", title: "Code of Conduct", subtitle: "example.com")
@@ -17,26 +17,20 @@
import XCTest
import RiotSwiftUI
class AuthenticationTermsUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationTermsScreenState.self
class AuthenticationTermsUITests: MockScreenTestCase {
func testMatrixDotOrg() {
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.matrixDotOrg.title)
verifyTerms(accepted: false)
}
override class func createTest() -> MockScreenTest {
return AuthenticationTermsUITests(selector: #selector(verifyAuthenticationTermsScreen))
func testAccepted() {
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.accepted.title)
verifyTerms(accepted: true)
}
func verifyAuthenticationTermsScreen() throws {
guard let screenState = screenState as? MockAuthenticationTermsScreenState else { fatalError("no screen") }
switch screenState {
case .matrixDotOrg:
verifyTerms(accepted: false)
case .accepted:
verifyTerms(accepted: true)
case .multiple:
verifyTerms(accepted: false)
}
func testMultiple() {
app.goToScreenWithIdentifier(MockAuthenticationTermsScreenState.multiple.title)
verifyTerms(accepted: false)
}
func verifyTerms(accepted: Bool) {
@@ -62,7 +62,7 @@ struct AuthenticationTermsScreen: View {
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.authenticationTermsMessage)
Text(viewModel.viewState.headerMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
@@ -32,11 +32,18 @@ enum AuthenticationVerifyEmailViewModelResult {
// MARK: View
struct AuthenticationVerifyEmailViewState: BindableState {
/// The homeserver requesting email verification.
let homeserver: AuthenticationHomeserverViewData
/// An email has been sent and the app is waiting for the user to tap the link.
var hasSentEmail = false
/// View state that can be bound to from SwiftUI.
var bindings: AuthenticationVerifyEmailBindings
/// The message shown in the header while asking for an email address to be entered.
var formHeaderMessage: String {
VectorL10n.authenticationVerifyEmailInputMessage(homeserver.address)
}
/// Whether the email address is valid and the user can continue.
var hasInvalidAddress: Bool {
bindings.emailAddress.isEmpty
@@ -31,8 +31,9 @@ class AuthenticationVerifyEmailViewModel: AuthenticationVerifyEmailViewModelType
// MARK: - Setup
init(emailAddress: String = "") {
let viewState = AuthenticationVerifyEmailViewState(bindings: AuthenticationVerifyEmailBindings(emailAddress: emailAddress))
init(homeserver: AuthenticationHomeserverViewData, emailAddress: String = "") {
let viewState = AuthenticationVerifyEmailViewState(homeserver: homeserver,
bindings: AuthenticationVerifyEmailBindings(emailAddress: emailAddress))
super.init(initialViewState: viewState)
}
@@ -19,6 +19,8 @@ import CommonKit
struct AuthenticationVerifyEmailCoordinatorParameters {
let registrationWizard: RegistrationWizard
/// The homeserver that is requesting email verification.
let homeserver: AuthenticationState.Homeserver
}
final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
@@ -54,7 +56,7 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
@MainActor init(parameters: AuthenticationVerifyEmailCoordinatorParameters) {
self.parameters = parameters
let viewModel = AuthenticationVerifyEmailViewModel()
let viewModel = AuthenticationVerifyEmailViewModel(homeserver: parameters.homeserver.viewData)
let view = AuthenticationVerifyEmailScreen(viewModel: viewModel.context)
authenticationVerifyEmailViewModel = viewModel
authenticationVerifyEmailHostingController = VectorHostingController(rootView: view)
@@ -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()
@@ -37,11 +37,14 @@ enum MockAuthenticationVerifyEmailScreenState: MockScreenState, CaseIterable {
let viewModel: AuthenticationVerifyEmailViewModel
switch self {
case .emptyAddress:
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "")
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "")
case .enteredAddress:
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "test@example.com")
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "test@example.com")
case .hasSentEmail:
viewModel = AuthenticationVerifyEmailViewModel(emailAddress: "test@example.com")
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg,
emailAddress: "test@example.com")
Task { await viewModel.updateForSentEmail() }
}
@@ -17,35 +17,17 @@
import XCTest
import RiotSwiftUI
class AuthenticationVerifyEmailUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockAuthenticationVerifyEmailScreenState.self
}
override class func createTest() -> MockScreenTest {
return AuthenticationVerifyEmailUITests(selector: #selector(verifyAuthenticationVerifyEmailScreen))
}
func verifyAuthenticationVerifyEmailScreen() throws {
guard let screenState = screenState as? MockAuthenticationVerifyEmailScreenState else { fatalError("no screen") }
switch screenState {
case .emptyAddress:
verifyEmptyAddress()
case .enteredAddress:
verifyEnteredAddress()
case .hasSentEmail:
verifyWaitingForEmailLink()
}
}
func verifyEmptyAddress() {
class AuthenticationVerifyEmailUITests: MockScreenTestCase {
func testEmptyAddress() {
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.emptyAddress.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
let addressTextField = app.textFields["addressTextField"]
XCTAssertTrue(addressTextField.exists, "The text field should be shown before an email is sent.")
XCTAssertEqual(addressTextField.value as? String, "Email Address", "The text field should be showing the placeholder before text is input.")
XCTAssertEqual(addressTextField.value as? String, VectorL10n.authenticationVerifyEmailTextFieldPlaceholder,
"The text field should be showing the placeholder before text is input.")
let nextButton = app.buttons["nextButton"]
XCTAssertTrue(nextButton.exists, "The next button should be shown before an email is sent.")
@@ -59,7 +41,9 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {
XCTAssertEqual(cancelButton.label, "Cancel")
}
func verifyEnteredAddress() {
func testEnteredAddress() {
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.enteredAddress.title)
XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown before an email is sent.")
XCTAssertTrue(app.staticTexts["messageLabel"].exists, "The message should be shown before an email is sent.")
@@ -79,7 +63,9 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {
XCTAssertEqual(cancelButton.label, "Cancel")
}
func verifyWaitingForEmailLink() {
func testWaitingForEmailLink() {
app.goToScreenWithIdentifier(MockAuthenticationVerifyEmailScreenState.hasSentEmail.title)
XCTAssertFalse(app.staticTexts["titleLabel"].exists, "The title should be hidden once an email has been sent.")
XCTAssertFalse(app.staticTexts["messageLabel"].exists, "The message should be hidden once an email has been sent.")
XCTAssertFalse(app.textFields["addressTextField"].exists, "The text field should be hidden once an email has been sent.")
@@ -24,7 +24,7 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase {
var context: AuthenticationVerifyEmailViewModelType.Context!
override func setUpWithError() throws {
viewModel = AuthenticationVerifyEmailViewModel()
viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg)
context = viewModel.context
}
@@ -55,7 +55,7 @@ struct AuthenticationVerifyEmailForm: View {
.foregroundColor(theme.colors.primaryContent)
.accessibilityIdentifier("titleLabel")
Text(VectorL10n.authenticationVerifyEmailInputMessage)
Text(viewModel.viewState.formHeaderMessage)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)

Some files were not shown because too many files have changed in this diff Show More