From 37a08c9d3d35930fc1691f19bed35678eee822a5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 12 Jul 2022 19:31:10 +0100 Subject: [PATCH 01/42] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 72a69d031..e1894029b 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.8.21 -CURRENT_PROJECT_VERSION = 1.8.21 +MARKETING_VERSION = 1.8.22 +CURRENT_PROJECT_VERSION = 1.8.22 From d6ee0f2d1081d1964f79b58230317a2b61fda8b3 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 13 Jul 2022 14:01:02 +0100 Subject: [PATCH 02/42] Remove ffmpeg again with opus libraries fixed to add arm64 sim support. --- .../xcshareddata/xcschemes/Riot.xcscheme | 2 +- .../xcshareddata/swiftpm/Package.resolved | 27 ++ Riot/Assets/third_party_licenses.html | 318 ++++++++---------- .../VoiceMessageAudioConverter.swift | 115 +++---- Riot/target.yml | 1 + changelog.d/6419.change | 1 + project.yml | 3 + 7 files changed, 220 insertions(+), 247 deletions(-) create mode 100644 changelog.d/6419.change diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index f973b344c..a9bea1d96 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -1,7 +1,7 @@ + version = "1.3">

- This application is making use of the following third party softwares: + This application is making use of the following third party softwares:

-
    +
    • MatrixSDK (https://github.com/matrix-org/matrix-ios-sdk.git) @@ -42,38 +42,38 @@

      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.

    • -
    • +
    • AFNetworking (https://github.com/AFNetworking/AFNetworking) -

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

      It is released under the MIT license. +

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

      It is released under the MIT license.
      Copyright (c) 2011-2016 Alamofire Software Foundation (https://alamofire.org/) -

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

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

      -
    • -
    • +

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

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

      +
    • +
    • HPGrowingTextView (https://github.com/HansPinckaers/GrowingTextView) -

      Multi-line/Autoresizing UITextView similar as in the SMS-app. -

      It is released under the MIT license. -
      Copyright (c) 2011 Hans Pinckaers -

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

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

      -
    • -
    • +

      Multi-line/Autoresizing UITextView similar as in the SMS-app. +

      It is released under the MIT license. +
      Copyright (c) 2011 Hans Pinckaers +

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

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

      +
    • +
    • libPhoneNumber-iOS (https://github.com/iziz/libPhoneNumber-iOS.git) -

      iOS library for parsing, formatting, storing and validating international phone numbers from libphonenumber library. -

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

      iOS library for parsing, formatting, storing and validating international phone numbers from libphonenumber library. +

      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:

      https://www.apache.org/licenses/LICENSE-2.0 -

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

      +

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

    • cmark (
    • Jitsi Meet iOS SDK (Jitsi Meet iOS SDK binaries) + href="https://github.com/jitsi/jitsi-meet-ios-sdk-releases">Jitsi Meet iOS SDK binaries)

      It is composed of 2 frameworks:

      • @@ -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.
      • +
      • + DGCollectionViewLeftAlignFlowLayout (https://github.com/Digipolitan/collection-view-left-align-flow-layout) +

        This is a simple layout that align does not try to fulfill the lines but stick elements to the left. +

        DGCollectionViewLeftAlignFlowLayout is licensed under the BSD 3-Clause license. +
        Copyright (c) 2017, Digipolitan All rights reserved. +

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

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

        +
      • KTCenterFlowLayout (https://github.com/keighl/KTCenterFlowLayout)

        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.

      • +
      • - ffmpeg-kit-ios-audio (https://github.com/tanersener/ffmpeg-kit) -

        -
        -                   GNU LESSER GENERAL PUBLIC LICENSE
        -                       Version 3, 29 June 2007
        +        swift-ogg (https://github.com/vector-im/swift-ogg)
        +        

        Makes use of code from 5 frameworks:

        +
          +
        • +

          It contains code adapted from:

          + watson-developer-cloud/swift-sdk (https://github.com/watson-developer-cloud/swift-sdk) +

          Copyright 2018-present 8x8, Inc. +

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

          https://www.apache.org/licenses/LICENSE-2.0 +

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

          +
        • +
        • + opus-swift (https://github.com/ybrid/opus-swift) +

          + MIT License - Copyright (C) 2007 Free Software Foundation, Inc. - 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. +

          +
        • +
        • + opus (https://opus-codec.org/downloads/) +

          + 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/ +

          +
        • +
        • + ogg-swift (https://github.com/ybrid/ogg-swift) +

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

          +
        • +
        • + ogg (https://xiph.org/downloads/) +

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

        +
      • +
    • WeakDictionary (https://github.com/nicholascross/WeakDictionary/) @@ -1900,8 +1860,6 @@ Library.
    • UICollectionViewRightAlignedLayout (https://github.com/mokagio/UICollectionViewRightAlignedLayout) -
      - UICollectionViewLeftAlignedLayout (https://github.com/mokagio/UICollectionViewLeftAlignedLayout)

      The MIT License (MIT)

      diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 9b3f85cec..7a21edcf6 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -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) { - 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) { - 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) -> 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) { - 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) { + 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) -> 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 } } } diff --git a/Riot/target.yml b/Riot/target.yml index 5a0505ad7..d8ee465a3 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -38,6 +38,7 @@ targets: - package: DesignKit - package: Mapbox - package: OrderedCollections + - package: SwiftOGG configFiles: Debug: Debug.xcconfig diff --git a/changelog.d/6419.change b/changelog.d/6419.change new file mode 100644 index 000000000..350f4ca20 --- /dev/null +++ b/changelog.d/6419.change @@ -0,0 +1 @@ +Replaces the usage of ffmpeg in the app again(Change was previously reverted). diff --git a/project.yml b/project.yml index fb4a0d0ba..77e49fb20 100644 --- a/project.yml +++ b/project.yml @@ -50,3 +50,6 @@ packages: url: https://github.com/apple/swift-collections minVersion: 1.0.2 maxVersion: 2.0.0 + SwiftOGG: + url: https://github.com/vector-im/swift-ogg + branch: langleyd/sim_arm64_suppport From c5e03e950df90e1a66a9e5d18745671e65722c12 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 13 Jul 2022 14:35:35 +0100 Subject: [PATCH 03/42] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index e1894029b..35bf08ea9 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.8.22 -CURRENT_PROJECT_VERSION = 1.8.22 +MARKETING_VERSION = 1.8.23 +CURRENT_PROJECT_VERSION = 1.8.23 From 8eecbf8ccaf572a86531cb2ae0bd205c9e410324 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 13 Jul 2022 17:06:43 +0100 Subject: [PATCH 04/42] Update upstream dependencies --- Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 137f96bef..821ba4dda 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/ogg-swift.git", "state" : { - "revision" : "803f94f7e2379c41ab0c645b5f95e6c933e73ce0", - "version" : "0.8.1" + "revision" : "9d82ed838404f10b607a1a1689f404563e9115c3", + "version" : "0.8.3" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/opus-swift", "state" : { - "revision" : "4c1bbe02b2f092f26562eb66fd77995de81abb48", - "version" : "0.8.1" + "revision" : "11f1887767cbc87c4b64b789ee830b779cc744cb", + "version" : "0.8.4" } }, { @@ -60,7 +60,7 @@ "location" : "https://github.com/vector-im/swift-ogg", "state" : { "branch" : "langleyd/sim_arm64_suppport", - "revision" : "0ba496281f2242970a098b548d45c1ca15f1cd2e" + "revision" : "a0b3bd2a58167d2248ca2fb8038f7268e88c6b99" } }, { From 021399127338fbbc0f811cb9fd813ac70a635c61 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 14 Jul 2022 15:28:21 +0100 Subject: [PATCH 05/42] Update swift-ogg with fix for device crash and remove unneeded change to xcscheme --- Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme | 2 +- Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index a9bea1d96..f973b344c 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> Date: Thu, 14 Jul 2022 16:03:19 +0100 Subject: [PATCH 06/42] Update to swift-ogg main branch --- Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- project.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index c4d61d779..899701b2f 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/swift-ogg", "state" : { - "branch" : "langleyd/sim_arm64_suppport", - "revision" : "6ef5ca6bc10ec5b7336e8c042accf76b17fa7a3c" + "branch" : "main", + "revision" : "0ffad3f7b45a6a4760db090d503b00f094bbecc0" } }, { diff --git a/project.yml b/project.yml index 77e49fb20..5ff5e65ef 100644 --- a/project.yml +++ b/project.yml @@ -52,4 +52,4 @@ packages: maxVersion: 2.0.0 SwiftOGG: url: https://github.com/vector-im/swift-ogg - branch: langleyd/sim_arm64_suppport + branch: main From f19149dab85e7b40d0542a9f07f5a2d0f7a46aa5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 12 Jul 2022 17:12:30 +0100 Subject: [PATCH 07/42] Move AuthenticationCoordinator into Authentication module. Reorganise moving AuthenticationViewController into a Legacy folder. --- .../AuthenticationCoordinator.swift | 0 .../AuthenticationType+Analytics.swift | 0 .../Authentication/{ => Legacy}/AuthenticationViewController.h | 0 .../Authentication/{ => Legacy}/AuthenticationViewController.m | 0 .../Authentication/{ => Legacy}/AuthenticationViewController.xib | 0 .../{ => Legacy}/LegacyAuthenticationCoordinator.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginButton.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginButtonFactory.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginButtonStyle.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginButtonViewData.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginListView.swift | 0 .../{ => Legacy}/SocialLogin/SocialLoginListView.xib | 0 Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.h | 0 Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.m | 0 Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.xib | 0 .../Authentication/{ => Legacy}/Views/ForgotPasswordInputsView.h | 0 .../Authentication/{ => Legacy}/Views/ForgotPasswordInputsView.m | 0 .../{ => Legacy}/Views/ForgotPasswordInputsView.xib | 0 Riot/Modules/Authentication/{ => Legacy}/Views/TermsView.swift | 0 Riot/Modules/Authentication/{ => Legacy}/Views/TermsView.xib | 0 .../SessionVerificationListener.swift | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename Riot/Modules/{Onboarding => Authentication}/AuthenticationCoordinator.swift (100%) rename Riot/Modules/{Onboarding => Authentication}/AuthenticationType+Analytics.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/AuthenticationViewController.h (100%) rename Riot/Modules/Authentication/{ => Legacy}/AuthenticationViewController.m (100%) rename Riot/Modules/Authentication/{ => Legacy}/AuthenticationViewController.xib (100%) rename Riot/Modules/Authentication/{ => Legacy}/LegacyAuthenticationCoordinator.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginButton.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginButtonFactory.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginButtonStyle.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginButtonViewData.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginListView.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/SocialLogin/SocialLoginListView.xib (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.h (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.m (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/AuthInputsView.xib (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/ForgotPasswordInputsView.h (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/ForgotPasswordInputsView.m (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/ForgotPasswordInputsView.xib (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/TermsView.swift (100%) rename Riot/Modules/Authentication/{ => Legacy}/Views/TermsView.xib (100%) rename Riot/Modules/{Onboarding => Authentication}/SessionVerificationListener.swift (100%) diff --git a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift similarity index 100% rename from Riot/Modules/Onboarding/AuthenticationCoordinator.swift rename to Riot/Modules/Authentication/AuthenticationCoordinator.swift diff --git a/Riot/Modules/Onboarding/AuthenticationType+Analytics.swift b/Riot/Modules/Authentication/AuthenticationType+Analytics.swift similarity index 100% rename from Riot/Modules/Onboarding/AuthenticationType+Analytics.swift rename to Riot/Modules/Authentication/AuthenticationType+Analytics.swift diff --git a/Riot/Modules/Authentication/AuthenticationViewController.h b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.h similarity index 100% rename from Riot/Modules/Authentication/AuthenticationViewController.h rename to Riot/Modules/Authentication/Legacy/AuthenticationViewController.h diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m similarity index 100% rename from Riot/Modules/Authentication/AuthenticationViewController.m rename to Riot/Modules/Authentication/Legacy/AuthenticationViewController.m diff --git a/Riot/Modules/Authentication/AuthenticationViewController.xib b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.xib similarity index 100% rename from Riot/Modules/Authentication/AuthenticationViewController.xib rename to Riot/Modules/Authentication/Legacy/AuthenticationViewController.xib diff --git a/Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift similarity index 100% rename from Riot/Modules/Authentication/LegacyAuthenticationCoordinator.swift rename to Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButton.swift similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginButton.swift rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButton.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonFactory.swift similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginButtonFactory.swift rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonFactory.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonStyle.swift b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonStyle.swift similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginButtonStyle.swift rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonStyle.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonViewData.swift similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginButtonViewData.swift rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginButtonViewData.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginListView.swift similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginListView.swift rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginListView.swift diff --git a/Riot/Modules/Authentication/SocialLogin/SocialLoginListView.xib b/Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginListView.xib similarity index 100% rename from Riot/Modules/Authentication/SocialLogin/SocialLoginListView.xib rename to Riot/Modules/Authentication/Legacy/SocialLogin/SocialLoginListView.xib diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.h b/Riot/Modules/Authentication/Legacy/Views/AuthInputsView.h similarity index 100% rename from Riot/Modules/Authentication/Views/AuthInputsView.h rename to Riot/Modules/Authentication/Legacy/Views/AuthInputsView.h diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.m b/Riot/Modules/Authentication/Legacy/Views/AuthInputsView.m similarity index 100% rename from Riot/Modules/Authentication/Views/AuthInputsView.m rename to Riot/Modules/Authentication/Legacy/Views/AuthInputsView.m diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.xib b/Riot/Modules/Authentication/Legacy/Views/AuthInputsView.xib similarity index 100% rename from Riot/Modules/Authentication/Views/AuthInputsView.xib rename to Riot/Modules/Authentication/Legacy/Views/AuthInputsView.xib diff --git a/Riot/Modules/Authentication/Views/ForgotPasswordInputsView.h b/Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.h similarity index 100% rename from Riot/Modules/Authentication/Views/ForgotPasswordInputsView.h rename to Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.h diff --git a/Riot/Modules/Authentication/Views/ForgotPasswordInputsView.m b/Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.m similarity index 100% rename from Riot/Modules/Authentication/Views/ForgotPasswordInputsView.m rename to Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.m diff --git a/Riot/Modules/Authentication/Views/ForgotPasswordInputsView.xib b/Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.xib similarity index 100% rename from Riot/Modules/Authentication/Views/ForgotPasswordInputsView.xib rename to Riot/Modules/Authentication/Legacy/Views/ForgotPasswordInputsView.xib diff --git a/Riot/Modules/Authentication/Views/TermsView.swift b/Riot/Modules/Authentication/Legacy/Views/TermsView.swift similarity index 100% rename from Riot/Modules/Authentication/Views/TermsView.swift rename to Riot/Modules/Authentication/Legacy/Views/TermsView.swift diff --git a/Riot/Modules/Authentication/Views/TermsView.xib b/Riot/Modules/Authentication/Legacy/Views/TermsView.xib similarity index 100% rename from Riot/Modules/Authentication/Views/TermsView.xib rename to Riot/Modules/Authentication/Legacy/Views/TermsView.xib diff --git a/Riot/Modules/Onboarding/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift similarity index 100% rename from Riot/Modules/Onboarding/SessionVerificationListener.swift rename to Riot/Modules/Authentication/SessionVerificationListener.swift From 5394ed0f4ab908b4a600b7276e6ff264104e013f Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 13 Jul 2022 13:39:27 +0100 Subject: [PATCH 08/42] Update use case, registration and login screens. Remove "Custom server" button from use case screen. Remove matrix.org description. Add username availability to registration screen. --- Riot/Assets/en.lproj/Untranslated.strings | 11 +- Riot/Assets/en.lproj/Vector.strings | 4 +- Riot/Generated/Strings.swift | 4 +- Riot/Generated/UntranslatedStrings.swift | 12 +- .../AuthenticationCoordinator.swift | 26 ++-- .../AuthenticationCoordinatorProtocol.swift | 3 - .../LegacyAuthenticationCoordinator.swift | 7 - .../Onboarding/OnboardingCoordinator.swift | 12 +- .../AuthenticationHomeserverViewData.swift | 6 - .../AuthenticationServerInfoSection.swift | 16 +-- .../MatrixSDK/AuthenticationState.swift | 1 - .../Test/UI/AuthenticationLoginUITests.swift | 17 --- .../View/AuthenticationLoginScreen.swift | 8 +- .../AuthenticationRegistrationModels.swift | 45 +++++-- .../AuthenticationRegistrationViewModel.swift | 19 ++- ...icationRegistrationViewModelProtocol.swift | 4 + ...uthenticationRegistrationCoordinator.swift | 1 + ...uthenticationRegistrationScreenState.swift | 1 + .../AuthenticationRegistrationUITests.swift | 28 ++-- ...enticationRegistrationViewModelTests.swift | 125 +++++++++++++----- .../AuthenticationRegistrationScreen.swift | 19 +-- ...nboardingUseCaseSelectionCoordinator.swift | 4 +- .../UseCase/OnboardingUseCaseModels.swift | 1 - .../OnboardingUseCaseSelectionScreen.swift | 58 +++----- 24 files changed, 224 insertions(+), 208 deletions(-) diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index b87ff55be..0a6c17581 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -22,17 +22,17 @@ // MARK: Onboarding Authentication WIP "authentication_registration_title" = "Create your account"; -"authentication_registration_message" = "We’ll need some info to get you set up."; "authentication_registration_username" = "Username"; "authentication_registration_username_footer" = "You can’t change this later"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "Others can discover you %@"; "authentication_registration_password_footer" = "Must be 8 characters or more"; "authentication_login_title" = "Welcome back!"; "authentication_login_username" = "Username or Email"; "authentication_login_forgot_password" = "Forgot password"; -"authentication_server_info_title" = "Choose your server to store your data"; -"authentication_server_info_matrix_description" = "Join millions for free on the largest public server"; +"authentication_server_info_title" = "Where your conversations will live"; "authentication_server_selection_title" = "Choose your server"; "authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data."; @@ -46,6 +46,7 @@ "authentication_verify_email_input_message" = "This will help verify your account and enables password recovery."; "authentication_verify_email_text_field_placeholder" = "Email Address"; "authentication_verify_email_waiting_title" = "Check your email to verify."; +/* The placeholder will show the email address that was entered. */ "authentication_verify_email_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@"; "authentication_verify_email_waiting_hint" = "Did not receive an email?"; "authentication_verify_email_waiting_button" = "Resend email"; @@ -54,6 +55,7 @@ "authentication_forgot_password_input_message" = "We will send you a verification link."; "authentication_forgot_password_text_field_placeholder" = "Email Address"; "authentication_forgot_password_waiting_title" = "Check your email"; +/* The placeholder will show the email address that was entered. */ "authentication_forgot_password_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@"; "authentication_forgot_password_waiting_hint" = "Did not receive an email?"; "authentication_forgot_password_waiting_button" = "Resend email"; @@ -69,6 +71,7 @@ "authentication_verify_msisdn_text_field_placeholder" = "Phone Number"; "authentication_verify_msisdn_otp_text_field_placeholder" = "Verification Code"; "authentication_verify_msisdn_waiting_title" = "Confirm your phone number"; +/* The placeholder will show the phone number that was entered. */ "authentication_verify_msisdn_waiting_message" = "We just sent a code to %@. Enter it below to verify it’s you."; "authentication_verify_msisdn_waiting_button" = "Resend code"; "authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number"; @@ -82,7 +85,9 @@ // MARK: Password Validation "password_validation_info_header" = "Your password should meet the criteria below:"; "password_validation_error_header" = "Given password does not meet the criteria below:"; +/* The placeholder will show a number */ "password_validation_error_min_length" = "At least %d characters."; +/* The placeholder will show a number */ "password_validation_error_max_length" = "Not exceed %d characters."; "password_validation_error_contain_lowercase_letter" = "Contain a lower-case letter."; "password_validation_error_contain_uppercase_letter" = "Contain an upper-case letter."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index a32d1b96f..059146455 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -114,8 +114,8 @@ "onboarding_use_case_work_messaging" = "Teams"; "onboarding_use_case_community_messaging" = "Communities"; /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ -"onboarding_use_case_not_sure_yet" = "Not sure yet? You can %@"; -"onboarding_use_case_skip_button" = "skip this question"; +"onboarding_use_case_not_sure_yet" = "Not sure yet? %@"; +"onboarding_use_case_skip_button" = "Skip this question"; "onboarding_use_case_existing_server_message" = "Looking to join an existing server?"; "onboarding_use_case_existing_server_button" = "Connect to server"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f90273a30..495c7047c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4047,7 +4047,7 @@ public class VectorL10n: NSObject { public static var onboardingUseCaseMessage: String { return VectorL10n.tr("Vector", "onboarding_use_case_message") } - /// Not sure yet? You can %@ + /// Not sure yet? %@ public static func onboardingUseCaseNotSureYet(_ p1: String) -> String { return VectorL10n.tr("Vector", "onboarding_use_case_not_sure_yet", p1) } @@ -4055,7 +4055,7 @@ public class VectorL10n: NSObject { public static var onboardingUseCasePersonalMessaging: String { return VectorL10n.tr("Vector", "onboarding_use_case_personal_messaging") } - /// skip this question + /// Skip this question public static var onboardingUseCaseSkipButton: String { return VectorL10n.tr("Vector", "onboarding_use_case_skip_button") } diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index f69a82fbc..52fff40ad 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -78,10 +78,6 @@ public extension VectorL10n { static var authenticationRecaptchaMessage: String { return VectorL10n.tr("Untranslated", "authentication_recaptcha_message") } - /// We’ll need some info to get you set up. - static var authenticationRegistrationMessage: String { - return VectorL10n.tr("Untranslated", "authentication_registration_message") - } /// Must be 8 characters or more static var authenticationRegistrationPasswordFooter: String { return VectorL10n.tr("Untranslated", "authentication_registration_password_footer") @@ -98,11 +94,11 @@ public extension VectorL10n { static var authenticationRegistrationUsernameFooter: String { return VectorL10n.tr("Untranslated", "authentication_registration_username_footer") } - /// Join millions for free on the largest public server - static var authenticationServerInfoMatrixDescription: String { - return VectorL10n.tr("Untranslated", "authentication_server_info_matrix_description") + /// Others can discover you %@ + static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "authentication_registration_username_footer_available", p1) } - /// Choose your server to store your data + /// Where your conversations will live static var authenticationServerInfoTitle: String { return VectorL10n.tr("Untranslated", "authentication_server_info_title") } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 34fc81e94..88e5b897b 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -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") @@ -788,15 +784,9 @@ extension AuthenticationCoordinator: UIAdaptivePresentationControllerDelegate { // MARK: - Unused conformances extension AuthenticationCoordinator { - var customServerFieldsVisible: Bool { - get { false } - set { /* no-op */ } - } - func update(authenticationFlow: AuthenticationFlow) { // unused } - } // MARK: - AuthFallBackViewControllerDelegate diff --git a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift index f6f59e160..a606654f9 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift @@ -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) diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index 7b16e1b4b..cc717dcaa 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -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) { diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index e41df70c7..ce40cd3c5 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -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() @@ -630,7 +624,7 @@ extension OnboardingSplashScreenViewModelResult { extension OnboardingUseCaseViewModelResult { /// The result converted into the type stored in the user session. - var userSessionPropertyValue: UserSessionProperties.UseCase? { + var userSessionPropertyValue: UserSessionProperties.UseCase { switch self { case .personalMessaging: return .personalMessaging @@ -640,8 +634,6 @@ extension OnboardingUseCaseViewModelResult { return .communityMessaging case .skipped: return .skipped - case .customServer: - return nil } } } diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift index 4b56563c3..c5fdf1bb4 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift @@ -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: []) diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift index 7401d2259..36647cff4 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift @@ -27,7 +27,6 @@ struct AuthenticationServerInfoSection: View { // MARK: - Public let address: String - let showMatrixDotOrgInfo: Bool let editAction: () -> Void // MARK: - Views @@ -39,18 +38,9 @@ struct AuthenticationServerInfoSection: View { .foregroundColor(theme.colors.secondaryContent) HStack { - VStack(alignment: .leading, spacing: 2) { - Text(address) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - - if showMatrixDotOrgInfo { - Text(VectorL10n.authenticationServerInfoMatrixDescription) - .font(theme.fonts.caption1) - .foregroundColor(theme.colors.tertiaryContent) - .accessibilityIdentifier("serverDescriptionText") - } - } + Text(address) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) Spacer() diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift index e8b91a62b..cac59552f 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift @@ -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 ?? []) diff --git a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift index 3bfda9406..7824a24fa 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift @@ -32,12 +32,10 @@ class AuthenticationLoginUITests: MockScreenTest { switch screenState { case .matrixDotOrg: let state = "matrix.org" - validateServerDescriptionIsVisible(for: state) validateLoginFormIsVisible(for: state) validateSSOButtonsAreShown(for: state) case .passwordOnly: let state = "a password only server" - validateServerDescriptionIsHidden(for: state) validateLoginFormIsVisible(for: state) validateSSOButtonsAreHidden(for: state) @@ -47,7 +45,6 @@ class AuthenticationLoginUITests: MockScreenTest { validateNextButtonIsEnabled(for: state) case .ssoOnly: let state = "an SSO only server" - validateServerDescriptionIsHidden(for: state) validateLoginFormIsHidden(for: state) validateSSOButtonsAreShown(for: state) case .fallback: @@ -56,20 +53,6 @@ class AuthenticationLoginUITests: MockScreenTest { } } - /// Checks that the server description label is shown. - func validateServerDescriptionIsVisible(for state: String) { - let descriptionLabel = app.staticTexts["serverDescriptionText"] - - XCTAssertTrue(descriptionLabel.exists, "The server description should be shown for \(state).") - XCTAssertEqual(descriptionLabel.label, VectorL10n.authenticationServerInfoMatrixDescription, "The server description should be correct for \(state).") - } - - /// Checks that the server description label is hidden. - func validateServerDescriptionIsHidden(for state: String) { - let descriptionLabel = app.staticTexts["serverDescriptionText"] - XCTAssertFalse(descriptionLabel.exists, "The server description should be shown for \(state).") - } - /// Checks that the username and password text fields are shown along with the next button. func validateLoginFormIsVisible(for state: String) { let usernameTextField = app.textFields.element diff --git a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift index a0068465c..af9c67a37 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift @@ -37,15 +37,16 @@ struct AuthenticationLoginScreen: View { VStack(spacing: 0) { header .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) - .padding(.bottom, 36) + .padding(.bottom, 28) serverInfo .padding(.leading, 12) + .padding(.bottom, 16) Rectangle() .fill(theme.colors.quinaryContent) .frame(height: 1) - .padding(.vertical, 21) + .padding(.bottom, 22) if viewModel.viewState.homeserver.showLoginForm { loginForm @@ -86,8 +87,7 @@ struct AuthenticationLoginScreen: View { /// The sever information section that includes a button to select a different server. var serverInfo: some View { - AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address, - showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) { + AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address) { viewModel.send(viewAction: .selectServer) } } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift index 531cb1a67..946e4ac54 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift @@ -50,6 +50,15 @@ enum AuthenticationRegistrationViewModelResult: CustomStringConvertible { // MARK: View struct AuthenticationRegistrationViewState: BindableState { + enum UsernameAvailability { + /// The availability of the username is unknown. + case unknown + /// The username is available. + case available + /// The username is invalid for the following reason. + case invalid(String) + } + /// Data about the selected homeserver. var homeserver: AuthenticationHomeserverViewData /// View state that can be bound to from SwiftUI. @@ -63,12 +72,20 @@ struct AuthenticationRegistrationViewState: BindableState { /// This is used to delay showing an error state until the user has tried 1 password. var hasEditedPassword = false - /// An error message to be shown in the username text field footer. - var usernameErrorMessage: String? + /// The availability of the currently enetered username. + var usernameAvailability: UsernameAvailability = .unknown /// The message to show in the username text field footer. var usernameFooterMessage: String { - usernameErrorMessage ?? VectorL10n.authenticationRegistrationUsernameFooter + switch usernameAvailability { + case .unknown: + return VectorL10n.authenticationRegistrationUsernameFooter + case .invalid(let errorMessage): + return errorMessage + case .available: + let userID = "@\(bindings.username):\(homeserver.address)" + return VectorL10n.authenticationRegistrationUsernameFooterAvailable(userID) + } } /// Whether to show any SSO buttons. @@ -76,19 +93,23 @@ struct AuthenticationRegistrationViewState: BindableState { !homeserver.ssoIdentityProviders.isEmpty } - /// Whether the current `username` is valid. - var isUsernameValid: Bool { - !bindings.username.isEmpty && usernameErrorMessage == nil + /// Whether the current `username` is invalid. + var isUsernameInvalid: Bool { + if case .invalid = usernameAvailability { + return true + } else { + return bindings.username.isEmpty + } } - /// Whether the current `password` is valid. - var isPasswordValid: Bool { - bindings.password.count >= 8 + /// Whether the current `password` is invalid. + var isPasswordInvalid: Bool { + bindings.password.count < 8 } /// `true` if it is possible to continue, otherwise `false`. var hasValidCredentials: Bool { - isUsernameValid && isPasswordValid + !isUsernameInvalid && !isPasswordInvalid } } @@ -108,8 +129,8 @@ enum AuthenticationRegistrationViewAction { case validateUsername /// Allows password validation to take place (sent after editing the password for the first time). case enablePasswordValidation - /// Clear any errors being shown in the username text field footer. - case clearUsernameError + /// Clear any availability messages being shown in the username text field footer. + case resetUsernameAvailability /// Continue using the input username and password. case next /// Continue using the supplied SSO provider. diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift index 73ec90f16..84e66cb48 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift @@ -48,8 +48,8 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy Task { await validateUsername() } case .enablePasswordValidation: Task { await enablePasswordValidation() } - case .clearUsernameError: - Task { await clearUsernameError() } + case .resetUsernameAvailability: + Task { await resetUsernameAvailability() } case .next: Task { await callback?(.createAccount(username: state.bindings.username, password: state.bindings.password)) } case .continueWithSSO(let provider): @@ -63,10 +63,15 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy state.homeserver = homeserver } + @MainActor func confirmUsernameAvailability(_ username: String) { + guard username == state.bindings.username else { return } + state.usernameAvailability = .available + } + @MainActor func displayError(_ type: AuthenticationRegistrationErrorType) { switch type { case .usernameUnavailable(let message): - state.usernameErrorMessage = message + state.usernameAvailability = .invalid(message) case .mxError(let message): state.bindings.alertInfo = AlertInfo(id: type, title: VectorL10n.error, @@ -101,9 +106,9 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy state.hasEditedPassword = true } - /// Clear any errors being shown in the username text field footer. - @MainActor private func clearUsernameError() { - guard state.usernameErrorMessage != nil else { return } - state.usernameErrorMessage = nil + /// Reset the username's availability, clearing any messages being shown in the username text field footer. + @MainActor private func resetUsernameAvailability() { + if case .unknown = state.usernameAvailability { return } + state.usernameAvailability = .unknown } } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift index e292edf8e..5fb4d1818 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift @@ -25,6 +25,10 @@ protocol AuthenticationRegistrationViewModelProtocol { /// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`. @MainActor func update(homeserver: AuthenticationHomeserverViewData) + /// Update the view to confirm that the chosen username is available. + /// - Parameter username: The username that was checked. + @MainActor func confirmUsernameAvailability(_ username: String) + /// Display an error to the user. /// - Parameter type: The type of error to be displayed. @MainActor func displayError(_ type: AuthenticationRegistrationErrorType) diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift index d1adfb74b..d052edab0 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift @@ -149,6 +149,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { currentTask = Task { do { _ = try await registrationWizard.registrationAvailable(username: username) + authenticationRegistrationViewModel.confirmUsernameAvailability(username) } catch { guard !Task.isCancelled, let mxError = MXError(nsError: error as NSError) else { return } if mxError.errcode == kMXErrCodeStringUserInUse diff --git a/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift index 2c0c280ef..a85021a17 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift @@ -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 diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift index 3725aa0ab..a94ee11df 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift @@ -36,7 +36,8 @@ class AuthenticationRegistrationUITests: MockScreenTest { validateSSOButtonsAreShown(for: state) validateFallbackButtonIsHidden(for: state) - validateNoErrorsAreShown(for: state) + validateUnknownUsernameAvailability(for: state) + validateNoPasswordErrorsAreShown(for: state) case .passwordOnly: let state = "a password only server" validateRegistrationFormIsVisible(for: state) @@ -45,7 +46,8 @@ class AuthenticationRegistrationUITests: MockScreenTest { validateNextButtonIsDisabled(for: state) - validateNoErrorsAreShown(for: state) + validateUnknownUsernameAvailability(for: state) + validateNoPasswordErrorsAreShown(for: state) case .passwordWithCredentials: let state = "a password only server with credentials entered" validateRegistrationFormIsVisible(for: state) @@ -54,7 +56,8 @@ class AuthenticationRegistrationUITests: MockScreenTest { validateNextButtonIsEnabled(for: state) - validateNoErrorsAreShown(for: state) + validateUsernameAvailable(for: state) + validateNoPasswordErrorsAreShown(for: state) case .passwordWithUsernameError: let state = "a password only server with an invalid username" validateRegistrationFormIsVisible(for: state) @@ -147,15 +150,24 @@ class AuthenticationRegistrationUITests: MockScreenTest { XCTAssertEqual(usernameFooter.label, VectorL10n.authInvalidUserName, "The username footer should be showing an error for \(state).") } - /// Checks that neither the username or password text field footers are showing an error. - func validateNoErrorsAreShown(for state: String) { + func validateUsernameAvailable(for state: String) { + let usernameFooter = textFieldFooter(for: "usernameTextField") + XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).") + XCTAssertTrue(usernameFooter.label.starts(with: VectorL10n.authenticationRegistrationUsernameFooterAvailable("")), + "The username footer should be showing the username as available for \(state).") + } + + func validateUnknownUsernameAvailability(for state: String) { let usernameFooter = textFieldFooter(for: "usernameTextField") - let passwordFooter = textFieldFooter(for: "passwordTextField") - XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).") - XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).") XCTAssertEqual(usernameFooter.label, VectorL10n.authenticationRegistrationUsernameFooter, "The username footer should be showing the default message for \(state).") + } + + /// Checks that neither the username or password text field footers are showing an error. + func validateNoPasswordErrorsAreShown(for state: String) { + let passwordFooter = textFieldFooter(for: "passwordTextField") + XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).") XCTAssertEqual(passwordFooter.label, VectorL10n.authenticationRegistrationPasswordFooter, "The password footer should be showing the default message for \(state).") } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift index d6565b2f8..b40e91b85 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift @@ -63,40 +63,89 @@ import Combine } func testUsernameError() async throws { - // Given a form with a valid username. + // Given a form with an entered username. context.username = "bob" - XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error when the view model is created.") + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.") XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.") - XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid if there is no error.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.") // When displaying the error as a username error. let errorMessage = "Username unavailable" viewModel.displayError(.usernameUnavailable(errorMessage)) // Then the error should be shown in the footer. - XCTAssertEqual(context.viewState.usernameErrorMessage, errorMessage, "The error message should be stored.") + guard case let .invalid(displayedError) = context.viewState.usernameAvailability else { + XCTFail("The username should be invalid when an error is shown.") + return + } + XCTAssertEqual(displayedError, errorMessage, "The error message should match.") XCTAssertEqual(context.viewState.usernameFooterMessage, errorMessage, "The error message should replace the standard footer message.") - XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.") - + XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.") // When clearing the error. - context.send(viewAction: .clearUsernameError) + context.send(viewAction: .resetUsernameAvailability) // Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors. - try await Task.sleep(nanoseconds: 100_000_000) + await Task.yield() // Then the error should be hidden again. - XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error anymore.") + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.") XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.") - XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when an error is cleared.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.") + } + + func testUsernameAvailability() async throws { + // Given a form with an entered username. + context.username = "bob" + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.") + + // When updating the state for an available username + viewModel.confirmUsernameAvailability("bob") + + // Then the error should be shown in the footer. + XCTAssertEqual(context.viewState.usernameAvailability, .available, + "The username should be detected as available.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooterAvailable("@bob:matrix.org"), + "The footer message should display that the username is available.") + XCTAssertFalse(context.viewState.isUsernameInvalid, + "The username should continue to be valid when it is available.") + + // When clearing the error. + context.send(viewAction: .resetUsernameAvailability) + + // Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors. + await Task.yield() + + // Then the error should be hidden again. + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should return to an unknown state.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when an error is cleared.") + } + + func testUsernameAvailabilityWhenChanged() async throws { + // Given a form with an entered username. + context.username = "robert" + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should be unknown when the view model is created.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid if there is no error.") + + // When updating the state for an available username that was previously entered. + viewModel.confirmUsernameAvailability("bob") + + // Then the username should not be shown as available. + XCTAssertEqual(context.viewState.usernameAvailability, .unknown, "The username availability should not be updated.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should continue to be valid when unverified.") } func testEmptyUsernameWithShortPassword() { // Given a form with an empty username and password. XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") // When entering a password of 7 characters without a username. @@ -104,8 +153,8 @@ import Combine context.password = "1234567" // Then the credentials should remain invalid. - XCTAssertFalse(context.viewState.isPasswordValid, "A 7-character password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "A 7-character password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") } @@ -113,8 +162,8 @@ import Combine // Given a form with an empty username and password. XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") // When entering a password of 8 characters without a username. @@ -122,8 +171,8 @@ import Combine context.password = "12345678" // Then the password should be valid but the credentials should still be invalid. - XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") } @@ -131,8 +180,8 @@ import Combine // Given a form with an empty username and password. XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") // When entering a username without a password. @@ -140,8 +189,8 @@ import Combine context.password = "" // Then the username should be valid but the credentials should still be invalid. - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") } @@ -149,8 +198,8 @@ import Combine // Given a form with an empty username and password. XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") // When entering a username and password and encountering a username error @@ -161,8 +210,8 @@ import Combine viewModel.displayError(.usernameUnavailable(errorMessage)) // Then the password should be valid but the credentials should still be invalid. - XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") - XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.") + XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "The username should be invalid when an error is shown.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") } @@ -170,8 +219,8 @@ import Combine // Given a form with an empty username and password. XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameInvalid, "An empty username should be invalid.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") // When entering a username and an 8-character password. @@ -179,8 +228,24 @@ import Combine context.password = "12345678" // Then the credentials should be considered valid. - XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") - XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.") + XCTAssertFalse(context.viewState.isPasswordInvalid, "An 8-character password should be valid.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.") XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") } } + +extension AuthenticationRegistrationViewState.UsernameAvailability: Equatable { + public static func == (lhs: AuthenticationRegistrationViewState.UsernameAvailability, + rhs: AuthenticationRegistrationViewState.UsernameAvailability) -> Bool { + switch (lhs, rhs) { + case (.unknown, .unknown): + return true + case (.available, .available): + return true + case (.invalid, .invalid): + return true + default: + return false + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift index da80554bf..24afdcccf 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift @@ -35,15 +35,16 @@ struct AuthenticationRegistrationScreen: View { VStack(spacing: 0) { header .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) - .padding(.bottom, 36) + .padding(.bottom, 28) serverInfo .padding(.leading, 12) + .padding(.bottom, 16) Rectangle() .fill(theme.colors.quinaryContent) .frame(height: 1) - .padding(.vertical, 21) + .padding(.bottom, 22) if viewModel.viewState.homeserver.showRegistrationForm { registrationForm @@ -84,18 +85,12 @@ struct AuthenticationRegistrationScreen: View { .font(theme.fonts.title2B) .multilineTextAlignment(.center) .foregroundColor(theme.colors.primaryContent) - - Text(VectorL10n.authenticationRegistrationMessage) - .font(theme.fonts.body) - .multilineTextAlignment(.center) - .foregroundColor(theme.colors.secondaryContent) } } /// The sever information section that includes a button to select a different server. var serverInfo: some View { - AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address, - showMatrixDotOrgInfo: viewModel.viewState.homeserver.isMatrixDotOrg) { + AuthenticationServerInfoSection(address: viewModel.viewState.homeserver.address) { viewModel.send(viewAction: .selectServer) } } @@ -107,21 +102,21 @@ struct AuthenticationRegistrationScreen: View { placeHolder: VectorL10n.authenticationRegistrationUsername, text: $viewModel.username, footerText: viewModel.viewState.usernameFooterMessage, - isError: viewModel.viewState.hasEditedUsername && !viewModel.viewState.isUsernameValid, + isError: viewModel.viewState.hasEditedUsername && viewModel.viewState.isUsernameInvalid, isFirstResponder: false, configuration: UIKitTextInputConfiguration(returnKeyType: .next, autocapitalizationType: .none, autocorrectionType: .no), onEditingChanged: usernameEditingChanged, onCommit: { isPasswordFocused = true }) - .onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .clearUsernameError) } + .onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .resetUsernameAvailability) } .accessibilityIdentifier("usernameTextField") RoundedBorderTextField(title: nil, placeHolder: VectorL10n.authPasswordPlaceholder, text: $viewModel.password, footerText: VectorL10n.authenticationRegistrationPasswordFooter, - isError: viewModel.viewState.hasEditedPassword && !viewModel.viewState.isPasswordValid, + isError: viewModel.viewState.hasEditedPassword && viewModel.viewState.isPasswordInvalid, isFirstResponder: isPasswordFocused, configuration: UIKitTextInputConfiguration(returnKeyType: .done, isSecureTextEntry: true), diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift index 7c7382958..d3ef2a79f 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift @@ -57,9 +57,7 @@ final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable { MXLog.debug("[OnboardingUseCaseSelectionCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).") // Show a loading indicator which can be dismissed externally by calling `stop`. - if result != .customServer { - self.startLoading() - } + self.startLoading() self.completion?(result) } } diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift index c67798b6d..a7160ee67 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift @@ -29,7 +29,6 @@ enum OnboardingUseCaseViewModelResult { case workMessaging case communityMessaging case skipped - case customServer } // MARK: View diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift index 6c41e0260..f0fbbfa61 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift @@ -29,6 +29,23 @@ struct OnboardingUseCaseSelectionScreen: View { @ObservedObject var viewModel: OnboardingUseCaseViewModel.Context + var body: some View { + ScrollView { + VStack(spacing: 0) { + titleContent + .padding(.bottom, 36) + + useCaseButtons + } + .readableFrame() + .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) + .padding(.bottom, 8) + .padding(.horizontal, 16) + } + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) + } + /// The screen's title and instructions. var titleContent: some View { VStack(spacing: 8) { @@ -76,47 +93,6 @@ struct OnboardingUseCaseSelectionScreen: View { .padding(.top, 8) } } - - /// A footer showing a button to connect to a server. - var serverFooter: some View { - VStack(spacing: 14) { - Text(VectorL10n.onboardingUseCaseExistingServerMessage) - .font(theme.fonts.subheadline) - .multilineTextAlignment(.center) - .foregroundColor(theme.colors.tertiaryContent) - - Button { viewModel.send(viewAction: .answer(.customServer)) } label: { - Text(VectorL10n.onboardingUseCaseExistingServerButton) - .font(theme.fonts.body) - } - } - } - - var body: some View { - GeometryReader { geometry in - VStack { - ScrollView { - VStack(spacing: 0) { - titleContent - .padding(.bottom, 36) - - useCaseButtons - } - .readableFrame() - .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) - .padding(.bottom, 8) - .padding(.horizontal, 16) - } - - serverFooter - .padding(.horizontal, 16) - .padding(.top, 8) - .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36) - } - } - .background(theme.colors.background.ignoresSafeArea()) - .accentColor(theme.colors.accent) - } } // MARK: - Previews From d5b5101420942d5d449d15f4b5a0e13bd36b6621 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 11:41:36 +0100 Subject: [PATCH 09/42] Update remaining strings for FTUE. Include homeserver in view models/state as needed. Pre-fill the user's display name as set by the server. --- Riot/Assets/en.lproj/Untranslated.strings | 46 +++++----- Riot/Assets/en.lproj/Vector.strings | 8 +- Riot/Generated/Strings.swift | 8 +- Riot/Generated/UntranslatedStrings.swift | 86 +++++++++---------- .../AuthenticationCoordinator.swift | 9 +- .../AuthenticationForgotPasswordModels.swift | 7 ++ ...uthenticationForgotPasswordViewModel.swift | 5 +- ...henticationForgotPasswordCoordinator.swift | 4 +- ...henticationForgotPasswordScreenState.swift | 9 +- .../AuthenticationForgotPasswordUITests.swift | 3 +- ...ticationForgotPasswordViewModelTests.swift | 2 +- .../AuthenticationForgotPasswordForm.swift | 2 +- .../AuthenticationLoginCoordinator.swift | 3 +- .../View/AuthenticationLoginScreen.swift | 3 +- .../View/AuthenticationReCaptchaScreen.swift | 7 +- .../AuthenticationRegistrationModels.swift | 4 +- .../AuthenticationServerSelectionModels.swift | 11 ++- ...thenticationServerSelectionViewModel.swift | 3 +- ...enticationServerSelectionCoordinator.swift | 1 + ...enticationServerSelectionScreenState.swift | 9 ++ ...AuthenticationServerSelectionUITests.swift | 22 ++++- ...icationServerSelectionViewModelTests.swift | 10 +-- .../AuthenticationServerSelectionScreen.swift | 18 ++-- .../AuthenticationSoftLogoutCoordinator.swift | 3 +- .../Terms/AuthenticationTermsModels.swift | 6 ++ .../Terms/AuthenticationTermsViewModel.swift | 5 +- .../AuthenticationTermsCoordinator.swift | 8 +- .../MockAuthenticationTermsScreenState.swift | 8 +- .../View/AuthenticationTermsScreen.swift | 2 +- .../AuthenticationVerifyEmailModels.swift | 7 ++ .../AuthenticationVerifyEmailViewModel.swift | 5 +- ...AuthenticationVerifyEmailCoordinator.swift | 4 +- ...AuthenticationVerifyEmailScreenState.swift | 9 +- .../UI/AuthenticationVerifyEmailUITests.swift | 3 +- ...henticationVerifyEmailViewModelTests.swift | 2 +- .../View/AuthenticationVerifyEmailForm.swift | 2 +- .../AuthenticationVerifyMsisdnModels.swift | 7 ++ .../AuthenticationVerifyMsisdnViewModel.swift | 5 +- ...uthenticationVerifyMsisdnCoordinator.swift | 4 +- ...uthenticationVerifyMsisdnScreenState.swift | 13 ++- .../AuthenticationVerifyMsisdnUITests.swift | 6 +- ...enticationVerifyMsisdnViewModelTests.swift | 2 +- .../View/AuthenticationVerifyMsisdnForm.swift | 2 +- .../OnboardingCongratulationsModels.swift | 12 +++ .../OnboardingCongratulationsScreen.swift | 2 +- .../OnboardingDisplayNameCoordinator.swift | 2 +- .../View/OnboardingDisplayNameScreen.swift | 5 -- changelog.d/6427.wip | 1 + 48 files changed, 250 insertions(+), 155 deletions(-) create mode 100644 changelog.d/6427.wip diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index 0a6c17581..7493ebd66 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -29,58 +29,58 @@ "authentication_registration_password_footer" = "Must be 8 characters or more"; "authentication_login_title" = "Welcome back!"; -"authentication_login_username" = "Username or Email"; +"authentication_login_username" = "Username / Email / Phone"; "authentication_login_forgot_password" = "Forgot password"; "authentication_server_info_title" = "Where your conversations will live"; -"authentication_server_selection_title" = "Choose your server"; -"authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data."; -"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_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 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_input_title" = "Enter your email"; +"authentication_verify_email_input_message" = "%@ needs to verify your account"; +"authentication_verify_email_text_field_placeholder" = "Email"; "authentication_verify_email_waiting_title" = "Check your email to verify."; /* The placeholder will show the email address that was entered. */ -"authentication_verify_email_waiting_message" = "To confirm your email address, tap the button in the email we just sent to %@"; +"authentication_verify_email_waiting_message" = "To confirm your email, 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_input_title" = "Enter your email"; +"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" = "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_message" = "Follow the instructions sent to %@"; "authentication_forgot_password_waiting_button" = "Resend email"; "authentication_choose_password_input_title" = "Choose a new password"; -"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more."; +"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more"; "authentication_choose_password_text_field_placeholder" = "New Password"; "authentication_choose_password_signout_all_devices" = "Sign out of all devices"; "authentication_choose_password_submit_button" = "Reset Password"; "authentication_verify_msisdn_input_title" = "Enter your phone number"; -"authentication_verify_msisdn_input_message" = "This will help verify your account and enables password recovery."; +"authentication_verify_msisdn_input_message" = "%@ needs to verify your account"; "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_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" = "We just sent a code to %@. Enter it below to verify it’s you."; +"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" = "Privacy policy"; -"authentication_terms_message" = "Please read through T&C. You must accept in order to continue."; +"authentication_terms_title" = "Server policies"; +"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_message" = "This server would like to make sure you are not a robot"; +"authentication_recaptcha_title" = "Are you a human?"; // MARK: Password Validation "password_validation_info_header" = "Your password should meet the criteria below:"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 059146455..d3d04f90e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -109,7 +109,7 @@ "onboarding_splash_page_4_message" = "Element is also great for the workplace. It’s trusted by the world’s most secure organisations."; "onboarding_use_case_title" = "Who will you chat to the most?"; -"onboarding_use_case_message" = "We’ll help you get connected."; +"onboarding_use_case_message" = "We’ll help you get connected"; "onboarding_use_case_personal_messaging" = "Friends and family"; "onboarding_use_case_work_messaging" = "Teams"; "onboarding_use_case_community_messaging" = "Communities"; @@ -135,11 +135,11 @@ "onboarding_display_name_max_length" = "Your display name must be less than 256 characters"; "onboarding_avatar_title" = "Add a profile picture"; -"onboarding_avatar_message" = "You can change this anytime."; +"onboarding_avatar_message" = "Time to put a face to the name"; "onboarding_avatar_accessibility_label" = "Profile picture"; -"onboarding_celebration_title" = "You’re all set!"; -"onboarding_celebration_message" = "Your preferences have been saved."; +"onboarding_celebration_title" = "Looking good!"; +"onboarding_celebration_message" = "Head to settings anytime to update your profile."; "onboarding_celebration_button" = "Let's go"; // Authentication diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 495c7047c..3451b7778 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3927,7 +3927,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 +3939,11 @@ public class VectorL10n: NSObject { public static var onboardingCelebrationButton: String { return VectorL10n.tr("Vector", "onboarding_celebration_button") } - /// Your preferences have been saved. + /// Head to settings anytime to update your profile. public static var onboardingCelebrationMessage: String { return VectorL10n.tr("Vector", "onboarding_celebration_message") } - /// You’re all set! + /// Looking good! public static var onboardingCelebrationTitle: String { return VectorL10n.tr("Vector", "onboarding_celebration_title") } @@ -4043,7 +4043,7 @@ public class VectorL10n: NSObject { public static var onboardingUseCaseExistingServerMessage: String { return VectorL10n.tr("Vector", "onboarding_use_case_existing_server_message") } - /// We’ll help you get connected. + /// We’ll help you get connected public static var onboardingUseCaseMessage: String { return VectorL10n.tr("Vector", "onboarding_use_case_message") } diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index 52fff40ad..d757181b6 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -14,7 +14,7 @@ public extension VectorL10n { static var authenticationCancelFlowConfirmationMessage: String { return VectorL10n.tr("Untranslated", "authentication_cancel_flow_confirmation_message") } - /// Make sure it’s 8 characters or more. + /// Make sure it’s 8 characters or more static var authenticationChoosePasswordInputMessage: String { return VectorL10n.tr("Untranslated", "authentication_choose_password_input_message") } @@ -34,15 +34,15 @@ public extension VectorL10n { 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") + /// %@ will send you a verification link + static func authenticationForgotPasswordInputMessage(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_message", p1) } - /// Enter your email address + /// Enter your email static var authenticationForgotPasswordInputTitle: String { return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_title") } - /// Email Address + /// Email static var authenticationForgotPasswordTextFieldPlaceholder: String { return VectorL10n.tr("Untranslated", "authentication_forgot_password_text_field_placeholder") } @@ -50,15 +50,11 @@ public extension VectorL10n { 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 %@ + /// Follow the instructions sent to %@ static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String { return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_message", p1) } - /// Check your email + /// Check your email. static var authenticationForgotPasswordWaitingTitle: String { return VectorL10n.tr("Untranslated", "authentication_forgot_password_waiting_title") } @@ -70,13 +66,13 @@ public extension VectorL10n { static var authenticationLoginTitle: String { return VectorL10n.tr("Untranslated", "authentication_login_title") } - /// Username or Email + /// Username / Email / Phone 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") + /// Are you a human? + static var authenticationRecaptchaTitle: String { + return VectorL10n.tr("Untranslated", "authentication_recaptcha_title") } /// Must be 8 characters or more static var authenticationRegistrationPasswordFooter: String { @@ -106,43 +102,47 @@ public extension VectorL10n { 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") + /// What is the address of your server? + static var authenticationServerSelectionLoginMessage: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_login_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") + /// Connect to homeserver + static var authenticationServerSelectionLoginTitle: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_login_title") } - /// Server URL + /// What is the address of your server? This is like a home for all your data + static var authenticationServerSelectionRegisterMessage: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_register_message") + } + /// Select your homeserver + static var authenticationServerSelectionRegisterTitle: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_register_title") + } + /// Homeserver 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") + /// Please read %@’s terms and policies + static func authenticationTermsMessage(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "authentication_terms_message", p1) } /// 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 + /// Server policies 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") + /// %@ needs to verify your account + static func authenticationVerifyEmailInputMessage(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message", p1) } - /// Enter your email address + /// Enter your email static var authenticationVerifyEmailInputTitle: String { return VectorL10n.tr("Untranslated", "authentication_verify_email_input_title") } - /// Email Address + /// Email static var authenticationVerifyEmailTextFieldPlaceholder: String { return VectorL10n.tr("Untranslated", "authentication_verify_email_text_field_placeholder") } @@ -154,7 +154,7 @@ public extension VectorL10n { 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 %@ + /// To confirm your email, 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) } @@ -162,9 +162,9 @@ public extension VectorL10n { 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") + /// %@ needs to verify your account + static func authenticationVerifyMsisdnInputMessage(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_message", p1) } /// Enter your phone number static var authenticationVerifyMsisdnInputTitle: String { @@ -174,7 +174,7 @@ public extension VectorL10n { static var authenticationVerifyMsisdnInvalidPhoneNumber: String { return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_invalid_phone_number") } - /// Verification Code + /// Confirmation Code static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String { return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_otp_text_field_placeholder") } @@ -186,11 +186,11 @@ public extension VectorL10n { static var authenticationVerifyMsisdnWaitingButton: String { return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_button") } - /// We just sent a code to %@. Enter it below to verify it’s you. + /// A code was sent to %@ static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String { return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1) } - /// Confirm your phone number + /// Verify your phone number static var authenticationVerifyMsisdnWaitingTitle: String { return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_title") } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 88e5b897b..f34f50839 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -394,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) @@ -412,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) @@ -459,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) diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift index 1c7f0a388..788a33552 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift @@ -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 diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift index c2966c4ab..461e064a6 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift @@ -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) } diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift index 81cc08f97..3f19180cf 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift index c955e1518..1d126e998 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift @@ -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() } } diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift index 7daab5eed..06b1948f4 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift @@ -45,7 +45,8 @@ class AuthenticationForgotPasswordUITests: MockScreenTest { 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.") diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift index 4e047f0b7..c05dfccaa 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift @@ -24,7 +24,7 @@ class AuthenticationForgotPasswordViewModelTests: XCTestCase { var context: AuthenticationForgotPasswordViewModelType.Context! override func setUpWithError() throws { - viewModel = AuthenticationForgotPasswordViewModel() + viewModel = AuthenticationForgotPasswordViewModel(homeserver: .mockMatrixDotOrg) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift index 3165215e5..9c19f94fb 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index f73cf3ad8..97d3a42ca 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -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 } diff --git a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift index af9c67a37..c8db60acc 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift @@ -104,8 +104,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, diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift index bda2a39af..7deaaddc2 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift @@ -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) } } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift index 946e4ac54..607f9f197 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift @@ -83,7 +83,9 @@ struct AuthenticationRegistrationViewState: BindableState { case .invalid(let errorMessage): return errorMessage case .available: - let userID = "@\(bindings.username):\(homeserver.address)" + // 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) } } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift index 50fdc96a5..8f4a07f62 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift @@ -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. diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift index ca68ce068..8a5326593 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift @@ -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)) } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index 3ad2b4866..0f3888146 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -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 diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift index ac95c3d69..c7e2f12c7 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift @@ -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) } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift index 4908493f2..53f21e0d7 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift @@ -31,7 +31,9 @@ class AuthenticationServerSelectionUITests: MockScreenTest { guard let screenState = screenState as? MockAuthenticationServerSelectionScreenState else { fatalError("no screen") } switch screenState { case .matrix: - verifyNormalState() + verifyRegisterState() + case .login: + verifyLoginState() case .emptyAddress: verifyEmptyAddress() case .invalidAddress: @@ -41,7 +43,12 @@ class AuthenticationServerSelectionUITests: MockScreenTest { } } - func verifyNormalState() { + func verifyRegisterState() { + 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,13 +58,20 @@ 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 verifyLoginState() { + let title = app.staticTexts["headerTitle"] + XCTAssertEqual(title.label, VectorL10n.authenticationServerSelectionLoginTitle) + let message = app.staticTexts["headerMessage"] + XCTAssertEqual(message.label, VectorL10n.authenticationServerSelectionLoginMessage) + } + func verifyEmptyAddress() { let serverTextField = app.textFields.element XCTAssertEqual(serverTextField.value as? String, VectorL10n.authenticationServerSelectionServerUrl, "The text field should show placeholder text in this state.") diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift index bdab021d3..ed75e67aa 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift @@ -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.") } } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift index ea6596d3f..687d6ec92 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift @@ -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) { diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift index 632d0ac8e..5b2fba433 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift @@ -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 } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift index 33821c3d2..8ff128be2 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift index 6ef1c6050..e35366899 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift @@ -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 diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift index 04a3bac6f..d05e3a5aa 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift b/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift index 36374d5d1..0bb977c46 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift @@ -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") diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift index cffd8a0dd..d8241ef62 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift index f72182168..dfe29221b 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift @@ -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 diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift index e28ac99e9..fca58b047 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift @@ -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) } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift index dede95bee..8968cb304 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift index 12b66140c..3226f2f00 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift @@ -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() } } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift index 3913539c7..c80a0cdb8 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift @@ -45,7 +45,8 @@ class AuthenticationVerifyEmailUITests: MockScreenTest { 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.") diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift index fa2acc2e4..3b340a9bf 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift @@ -24,7 +24,7 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase { var context: AuthenticationVerifyEmailViewModelType.Context! override func setUpWithError() throws { - viewModel = AuthenticationVerifyEmailViewModel() + viewModel = AuthenticationVerifyEmailViewModel(homeserver: .mockMatrixDotOrg) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift index 12eae0941..274a0a942 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift index 86601fafc..c9d9aec6e 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift @@ -34,11 +34,18 @@ enum AuthenticationVerifyMsisdnViewModelResult { // MARK: View struct AuthenticationVerifyMsisdnViewState: BindableState { + /// The homeserver requesting MSISDN verification. + let homeserver: AuthenticationHomeserverViewData /// An SMS has been sent. var hasSentSMS = false /// View state that can be bound to from SwiftUI. var bindings: AuthenticationVerifyMsisdnBindings + /// The message shown in the header while asking for a phone number to be entered. + var formHeaderMessage: String { + VectorL10n.authenticationVerifyMsisdnInputMessage(homeserver.address) + } + /// Whether the phone number is valid and the user can continue. var hasInvalidPhoneNumber: Bool { bindings.phoneNumber.isEmpty diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift index d49be7562..de688374a 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift @@ -31,8 +31,9 @@ class AuthenticationVerifyMsisdnViewModel: AuthenticationVerifyMsisdnViewModelTy // MARK: - Setup - init(phoneNumber: String = "", otp: String = "") { - let viewState = AuthenticationVerifyMsisdnViewState(bindings: AuthenticationVerifyMsisdnBindings(phoneNumber: phoneNumber, otp: otp)) + init(homeserver: AuthenticationHomeserverViewData, phoneNumber: String = "", otp: String = "") { + let viewState = AuthenticationVerifyMsisdnViewState(homeserver: .mockMatrixDotOrg, + bindings: AuthenticationVerifyMsisdnBindings(phoneNumber: phoneNumber, otp: otp)) super.init(initialViewState: viewState) } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift index 82ed492a8..1962efe56 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift @@ -20,6 +20,8 @@ import libPhoneNumber_iOS struct AuthenticationVerifyMsisdnCoordinatorParameters { let registrationWizard: RegistrationWizard + /// The homeserver that is requesting MSISDN verification. + let homeserver: AuthenticationState.Homeserver } final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { @@ -55,7 +57,7 @@ final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { @MainActor init(parameters: AuthenticationVerifyMsisdnCoordinatorParameters) { self.parameters = parameters - let viewModel = AuthenticationVerifyMsisdnViewModel() + let viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: parameters.homeserver.viewData) let view = AuthenticationVerifyMsisdnScreen(viewModel: viewModel.context) authenticationVerifyMsisdnViewModel = viewModel authenticationVerifyMsisdnHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift index 8e86e9ff7..63242fa42 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift @@ -38,14 +38,19 @@ enum MockAuthenticationVerifyMsisdnScreenState: MockScreenState, CaseIterable { let viewModel: AuthenticationVerifyMsisdnViewModel switch self { case .emptyPhoneNumber: - viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "") + viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg, + phoneNumber: "") case .enteredPhoneNumber: - viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX") + viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg, + phoneNumber: "+44 XXXXXXXXX") case .hasSentSMS: - viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX") + viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg, + phoneNumber: "+44 XXXXXXXXX") Task { await viewModel.updateForSentSMS() } case .enteredOTP: - viewModel = AuthenticationVerifyMsisdnViewModel(phoneNumber: "+44 XXXXXXXXX", otp: "123456") + viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg, + phoneNumber: "+44 XXXXXXXXX", + otp: "123456") Task { await viewModel.updateForSentSMS() } } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift index cf7a4d543..a9fddacb8 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift @@ -50,7 +50,8 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest { let phoneNumberTextField = app.textFields["phoneNumberTextField"] XCTAssertTrue(phoneNumberTextField.exists, "The text field should be shown before an SMS is sent.") - XCTAssertEqual(phoneNumberTextField.value as? String, "Phone Number", "The text field should be showing the placeholder before text is input.") + XCTAssertEqual(phoneNumberTextField.value as? String, VectorL10n.authenticationVerifyMsisdnTextFieldPlaceholder, + "The text field should be showing the placeholder before text is input.") let nextButton = app.buttons["nextButton"] XCTAssertTrue(nextButton.exists, "The next button should be shown.") @@ -99,7 +100,8 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest { let otpTextField = app.textFields["otpTextField"] XCTAssertTrue(otpTextField.exists, "The OTP text field should be shown once an SMS has been sent.") - XCTAssertEqual(otpTextField.value as? String, "Verification Code", "The text field should be showing the placeholder before text is input.") + XCTAssertEqual(otpTextField.value as? String, VectorL10n.authenticationVerifyMsisdnOtpTextFieldPlaceholder, + "The text field should be showing the placeholder before text is input.") let nextButton = app.buttons["nextButton"] XCTAssertTrue(nextButton.exists, "The next button should be shown.") diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift index fb1e6a47d..77a117d37 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift @@ -24,7 +24,7 @@ class AuthenticationVerifyMsisdnViewModelTests: XCTestCase { var context: AuthenticationVerifyMsisdnViewModelType.Context! override func setUpWithError() throws { - viewModel = AuthenticationVerifyMsisdnViewModel() + viewModel = AuthenticationVerifyMsisdnViewModel(homeserver: .mockMatrixDotOrg) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift index 5f00427c6..9701a680d 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift @@ -55,7 +55,7 @@ struct AuthenticationVerifyMsisdnForm: View { .foregroundColor(theme.colors.primaryContent) .accessibilityIdentifier("titleLabel") - Text(VectorL10n.authenticationVerifyMsisdnInputMessage) + Text(viewModel.viewState.formHeaderMessage) .font(theme.fonts.body) .multilineTextAlignment(.center) .foregroundColor(theme.colors.secondaryContent) diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift index 1a83694dc..a94adf6f1 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift @@ -30,6 +30,18 @@ enum OnboardingCongratulationsViewModelResult { struct OnboardingCongratulationsViewState: BindableState { let userId: String let personalizationDisabled: Bool + + var messageString: NSAttributedString { + let message = VectorL10n.onboardingCongratulationsMessage(userId) + + let attributedMessage = NSMutableAttributedString(string: message) + let range = (message as NSString).range(of: userId) + if range.location != NSNotFound { + attributedMessage.addAttributes([.font: UIFont.element.body.bold], range: range) + } + + return attributedMessage + } } enum OnboardingCongratulationsViewAction { diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift index 687ea11b5..d0ca409b0 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift @@ -80,7 +80,7 @@ struct OnboardingCongratulationsScreen: View { .font(theme.fonts.title2B) .foregroundColor(.white) - Text(VectorL10n.onboardingCongratulationsMessage(viewModel.viewState.userId)) + StyledText(viewModel.viewState.messageString) .font(theme.fonts.body) .foregroundColor(.white) .multilineTextAlignment(.center) diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift index 4eb3e405e..feeab4018 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift @@ -46,7 +46,7 @@ final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { self.parameters = parameters // Don't pre-fill the display name from the MXID to encourage the user to enter something - let viewModel = OnboardingDisplayNameViewModel() + let viewModel = OnboardingDisplayNameViewModel(displayName: parameters.userSession.account.userDisplayName) let view = OnboardingDisplayNameScreen(viewModel: viewModel.context) onboardingDisplayNameViewModel = viewModel diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift index aa8a29d7c..005a33f2c 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift @@ -72,11 +72,6 @@ struct OnboardingDisplayNameScreen: View { .font(theme.fonts.title2B) .multilineTextAlignment(.center) .foregroundColor(theme.colors.primaryContent) - - Text(VectorL10n.onboardingDisplayNameMessage) - .font(theme.fonts.body) - .multilineTextAlignment(.center) - .foregroundColor(theme.colors.secondaryContent) } } diff --git a/changelog.d/6427.wip b/changelog.d/6427.wip new file mode 100644 index 000000000..f0f77f75e --- /dev/null +++ b/changelog.d/6427.wip @@ -0,0 +1 @@ +Update strings for FTUE authentication flow following final review. From bdc291169826af8fed271fd5b248a7c8f43af58c Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 14:07:43 +0100 Subject: [PATCH 10/42] Fix build error on MXKSendReplyEventStringLocalizer. --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../Models/Room/MXKSendReplyEventStringLocalizer.swift | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index d3d04f90e..f37d09cf4 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2445,6 +2445,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" = "has shared their live location."; "message_reply_to_message_to_reply_to_prefix" = "In reply to"; // Room members diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3451b7778..b250f9c24 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3231,6 +3231,10 @@ public class VectorL10n: NSObject { public static var messageReplyToSenderSentAnImage: String { return VectorL10n.tr("Vector", "message_reply_to_sender_sent_an_image") } + /// has shared their 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") diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift index 3df501245..f22c779fd 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift @@ -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 From 4dbefa2b67b5a2a0f7c202c213c8392777825480 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 14:13:32 +0100 Subject: [PATCH 11/42] Move FTUE strings for translation. --- Riot/Assets/en.lproj/Untranslated.strings | 74 -------- Riot/Assets/en.lproj/Vector.strings | 79 +++++++- Riot/Generated/Strings.swift | 216 ++++++++++++++++++++++ Riot/Generated/UntranslatedStrings.swift | 216 ---------------------- 4 files changed, 293 insertions(+), 292 deletions(-) diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index 7493ebd66..9ff00a53d 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -19,77 +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_username" = "Username"; -"authentication_registration_username_footer" = "You can’t change this later"; -/* The placeholder will show the full Matrix ID that has been entered. */ -"authentication_registration_username_footer_available" = "Others can discover you %@"; -"authentication_registration_password_footer" = "Must be 8 characters or more"; - -"authentication_login_title" = "Welcome back!"; -"authentication_login_username" = "Username / Email / Phone"; -"authentication_login_forgot_password" = "Forgot password"; - -"authentication_server_info_title" = "Where your conversations will 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"; -"authentication_verify_email_input_message" = "%@ needs to verify your account"; -"authentication_verify_email_text_field_placeholder" = "Email"; -"authentication_verify_email_waiting_title" = "Check your email to verify."; -/* The placeholder will show the email address that was entered. */ -"authentication_verify_email_waiting_message" = "To confirm your email, 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"; -"authentication_forgot_password_input_message" = "%@ will send you a verification link"; -"authentication_forgot_password_text_field_placeholder" = "Email"; -"authentication_forgot_password_waiting_title" = "Check your email."; -/* The placeholder will show the email address that was entered. */ -"authentication_forgot_password_waiting_message" = "Follow the instructions sent to %@"; -"authentication_forgot_password_waiting_button" = "Resend email"; - -"authentication_choose_password_input_title" = "Choose a new password"; -"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more"; -"authentication_choose_password_text_field_placeholder" = "New Password"; -"authentication_choose_password_signout_all_devices" = "Sign out of all devices"; -"authentication_choose_password_submit_button" = "Reset Password"; - -"authentication_verify_msisdn_input_title" = "Enter your phone number"; -"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"; -"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."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f37d09cf4..2bb17449a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -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."; @@ -142,7 +142,82 @@ "onboarding_celebration_message" = "Head to settings anytime to update your profile."; "onboarding_celebration_button" = "Let's go"; -// Authentication +// MARK: Authentication +"authentication_registration_title" = "Create your account"; +"authentication_registration_username" = "Username"; +"authentication_registration_username_footer" = "You can’t change this later"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "Others can discover you %@"; +"authentication_registration_password_footer" = "Must be 8 characters or more"; + +"authentication_login_title" = "Welcome back!"; +"authentication_login_username" = "Username / Email / Phone"; +"authentication_login_forgot_password" = "Forgot password"; + +"authentication_server_info_title" = "Where your conversations will 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"; +"authentication_verify_email_input_message" = "%@ needs to verify your account"; +"authentication_verify_email_text_field_placeholder" = "Email"; +"authentication_verify_email_waiting_title" = "Check your email to verify."; +/* The placeholder will show the email address that was entered. */ +"authentication_verify_email_waiting_message" = "To confirm your email, 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"; +"authentication_forgot_password_input_message" = "%@ will send you a verification link"; +"authentication_forgot_password_text_field_placeholder" = "Email"; +"authentication_forgot_password_waiting_title" = "Check your email."; +/* The placeholder will show the email address that was entered. */ +"authentication_forgot_password_waiting_message" = "Follow the instructions sent to %@"; +"authentication_forgot_password_waiting_button" = "Resend email"; + +"authentication_choose_password_input_title" = "Choose a new password"; +"authentication_choose_password_input_message" = "Make sure it’s 8 characters or more"; +"authentication_choose_password_text_field_placeholder" = "New Password"; +"authentication_choose_password_signout_all_devices" = "Sign out of all devices"; +"authentication_choose_password_submit_button" = "Reset Password"; + +"authentication_verify_msisdn_input_title" = "Enter your phone number"; +"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"; +"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"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index b250f9c24..1869c5851 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -531,6 +531,190 @@ public class VectorL10n: NSObject { public static var authenticatedSessionFlowNotSupported: String { return VectorL10n.tr("Vector", "authenticated_session_flow_not_supported") } + /// Your account is not created yet. Stop the registration process? + public static var authenticationCancelFlowConfirmationMessage: String { + return VectorL10n.tr("Vector", "authentication_cancel_flow_confirmation_message") + } + /// Make sure it’s 8 characters or more + public static var authenticationChoosePasswordInputMessage: String { + return VectorL10n.tr("Vector", "authentication_choose_password_input_message") + } + /// Choose a new password + public static var authenticationChoosePasswordInputTitle: String { + return VectorL10n.tr("Vector", "authentication_choose_password_input_title") + } + /// Sign out of all devices + public static var authenticationChoosePasswordSignoutAllDevices: String { + return VectorL10n.tr("Vector", "authentication_choose_password_signout_all_devices") + } + /// Reset Password + public static var authenticationChoosePasswordSubmitButton: String { + return VectorL10n.tr("Vector", "authentication_choose_password_submit_button") + } + /// New Password + public static var authenticationChoosePasswordTextFieldPlaceholder: String { + return VectorL10n.tr("Vector", "authentication_choose_password_text_field_placeholder") + } + /// %@ will send you a verification link + public static func authenticationForgotPasswordInputMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "authentication_forgot_password_input_message", p1) + } + /// Enter your email + public static var authenticationForgotPasswordInputTitle: String { + return VectorL10n.tr("Vector", "authentication_forgot_password_input_title") + } + /// Email + public static var authenticationForgotPasswordTextFieldPlaceholder: String { + return VectorL10n.tr("Vector", "authentication_forgot_password_text_field_placeholder") + } + /// Resend email + public static var authenticationForgotPasswordWaitingButton: String { + return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_button") + } + /// Follow the instructions sent to %@ + public static func authenticationForgotPasswordWaitingMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_message", p1) + } + /// Check your email. + public static var authenticationForgotPasswordWaitingTitle: String { + return VectorL10n.tr("Vector", "authentication_forgot_password_waiting_title") + } + /// Forgot password + public static var authenticationLoginForgotPassword: String { + return VectorL10n.tr("Vector", "authentication_login_forgot_password") + } + /// Welcome back! + public static var authenticationLoginTitle: String { + return VectorL10n.tr("Vector", "authentication_login_title") + } + /// Username / Email / Phone + public static var authenticationLoginUsername: String { + return VectorL10n.tr("Vector", "authentication_login_username") + } + /// Are you a human? + public static var authenticationRecaptchaTitle: String { + return VectorL10n.tr("Vector", "authentication_recaptcha_title") + } + /// Must be 8 characters or more + public static var authenticationRegistrationPasswordFooter: String { + return VectorL10n.tr("Vector", "authentication_registration_password_footer") + } + /// Create your account + public static var authenticationRegistrationTitle: String { + return VectorL10n.tr("Vector", "authentication_registration_title") + } + /// Username + public static var authenticationRegistrationUsername: String { + return VectorL10n.tr("Vector", "authentication_registration_username") + } + /// You can’t change this later + public static var authenticationRegistrationUsernameFooter: String { + return VectorL10n.tr("Vector", "authentication_registration_username_footer") + } + /// Others can discover you %@ + public static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String { + return VectorL10n.tr("Vector", "authentication_registration_username_footer_available", p1) + } + /// Where your conversations will live + public static var authenticationServerInfoTitle: String { + return VectorL10n.tr("Vector", "authentication_server_info_title") + } + /// 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") + } + /// To confirm your email, tap the button in the email we just sent to %@ + public static func authenticationVerifyEmailWaitingMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "authentication_verify_email_waiting_message", p1) + } + /// Check your email to verify. + 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") @@ -4079,6 +4263,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") diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index d757181b6..f273877eb 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -10,226 +10,10 @@ import Foundation // swiftlint:disable function_parameter_count identifier_name line_length type_body_length public extension VectorL10n { - /// Your account is not created yet. Stop the registration process? - static var authenticationCancelFlowConfirmationMessage: String { - return VectorL10n.tr("Untranslated", "authentication_cancel_flow_confirmation_message") - } - /// Make sure it’s 8 characters or more - static var authenticationChoosePasswordInputMessage: String { - return VectorL10n.tr("Untranslated", "authentication_choose_password_input_message") - } - /// Choose a new password - static var authenticationChoosePasswordInputTitle: String { - return VectorL10n.tr("Untranslated", "authentication_choose_password_input_title") - } - /// Sign out of all devices - static var authenticationChoosePasswordSignoutAllDevices: String { - return VectorL10n.tr("Untranslated", "authentication_choose_password_signout_all_devices") - } - /// Reset Password - static var authenticationChoosePasswordSubmitButton: String { - return VectorL10n.tr("Untranslated", "authentication_choose_password_submit_button") - } - /// New Password - static var authenticationChoosePasswordTextFieldPlaceholder: String { - return VectorL10n.tr("Untranslated", "authentication_choose_password_text_field_placeholder") - } - /// %@ will send you a verification link - static func authenticationForgotPasswordInputMessage(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_message", p1) - } - /// Enter your email - static var authenticationForgotPasswordInputTitle: String { - return VectorL10n.tr("Untranslated", "authentication_forgot_password_input_title") - } - /// Email - 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") - } - /// Follow the instructions 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 / Email / Phone - static var authenticationLoginUsername: String { - return VectorL10n.tr("Untranslated", "authentication_login_username") - } - /// Are you a human? - static var authenticationRecaptchaTitle: String { - return VectorL10n.tr("Untranslated", "authentication_recaptcha_title") - } - /// Must be 8 characters or more - static var authenticationRegistrationPasswordFooter: String { - return VectorL10n.tr("Untranslated", "authentication_registration_password_footer") - } - /// Create your account - static var authenticationRegistrationTitle: String { - return VectorL10n.tr("Untranslated", "authentication_registration_title") - } - /// Username - static var authenticationRegistrationUsername: String { - return VectorL10n.tr("Untranslated", "authentication_registration_username") - } - /// You can’t change this later - static var authenticationRegistrationUsernameFooter: String { - return VectorL10n.tr("Untranslated", "authentication_registration_username_footer") - } - /// Others can discover you %@ - static func authenticationRegistrationUsernameFooterAvailable(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_registration_username_footer_available", p1) - } - /// Where your conversations will live - 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? - static var authenticationServerSelectionLoginMessage: String { - return VectorL10n.tr("Untranslated", "authentication_server_selection_login_message") - } - /// Connect to homeserver - static var authenticationServerSelectionLoginTitle: String { - return VectorL10n.tr("Untranslated", "authentication_server_selection_login_title") - } - /// What is the address of your server? This is like a home for all your data - static var authenticationServerSelectionRegisterMessage: String { - return VectorL10n.tr("Untranslated", "authentication_server_selection_register_message") - } - /// Select your homeserver - static var authenticationServerSelectionRegisterTitle: String { - return VectorL10n.tr("Untranslated", "authentication_server_selection_register_title") - } - /// Homeserver URL - static var authenticationServerSelectionServerUrl: String { - return VectorL10n.tr("Untranslated", "authentication_server_selection_server_url") - } - /// Please read %@’s terms and policies - static func authenticationTermsMessage(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_terms_message", p1) - } - /// Unable to find the selected policy. Please try again later. - static var authenticationTermsPolicyUrlError: String { - return VectorL10n.tr("Untranslated", "authentication_terms_policy_url_error") - } - /// Server policies - static var authenticationTermsTitle: String { - return VectorL10n.tr("Untranslated", "authentication_terms_title") - } - /// %@ needs to verify your account - static func authenticationVerifyEmailInputMessage(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_verify_email_input_message", p1) - } - /// Enter your email - static var authenticationVerifyEmailInputTitle: String { - return VectorL10n.tr("Untranslated", "authentication_verify_email_input_title") - } - /// Email - 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, 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") - } - /// %@ needs to verify your account - static func authenticationVerifyMsisdnInputMessage(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_message", p1) - } - /// 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") - } - /// Confirmation 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") - } - /// A code was sent to %@ - static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String { - return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1) - } - /// Verify 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 From 85cdd41a2953a54b27a04ffda3509d1ca06d2060 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 15:12:37 +0100 Subject: [PATCH 12/42] Fix Riot tests. --- .../Authentication/AuthenticationServiceTests.swift | 4 ---- RiotTests/OnboardingTests.swift | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/RiotTests/Modules/Authentication/AuthenticationServiceTests.swift b/RiotTests/Modules/Authentication/AuthenticationServiceTests.swift index db499be20..5d9cae5bb 100644 --- a/RiotTests/Modules/Authentication/AuthenticationServiceTests.swift +++ b/RiotTests/Modules/Authentication/AuthenticationServiceTests.swift @@ -250,7 +250,6 @@ import XCTest // Then the view data should correctly represent the homeserver. XCTAssertEqual(viewData.address, "matrix.org", "The displayed address should match the address supplied by the user, but without the scheme.") - XCTAssertEqual(viewData.isMatrixDotOrg, true, "The server should be detected as matrix.org.") XCTAssertTrue(viewData.showLoginForm, "The login form should be shown.") XCTAssertEqual(viewData.ssoIdentityProviders, ssoIdentityProviders, "The sso identity providers should match.") XCTAssertTrue(viewData.showRegistrationForm, "The registration form should be shown.") @@ -270,7 +269,6 @@ import XCTest // Then the view data should correctly represent the homeserver. XCTAssertEqual(viewData.address, "example.com", "The displayed address should match the address supplied by the user, but without the scheme.") - XCTAssertEqual(viewData.isMatrixDotOrg, false, "The server should not be detected as matrix.org.") XCTAssertTrue(viewData.showLoginForm, "The login form should be shown.") XCTAssertEqual(viewData.ssoIdentityProviders, [], "There shouldn't be any sso identity providers.") XCTAssertFalse(viewData.showRegistrationForm, "The registration form should not be shown.") @@ -291,7 +289,6 @@ import XCTest // Then the view data should correctly represent the homeserver. XCTAssertEqual(viewData.address, "company.com", "The displayed address should match the address supplied by the user, but without the scheme.") - XCTAssertEqual(viewData.isMatrixDotOrg, false, "The server should not be detected as matrix.org.") XCTAssertFalse(viewData.showLoginForm, "The login form should not be shown.") XCTAssertEqual(viewData.ssoIdentityProviders, ssoIdentityProviders, "The sso identity providers should match.") XCTAssertFalse(viewData.showRegistrationForm, "The registration form should not be shown.") @@ -311,7 +308,6 @@ import XCTest // Then the view data should correctly represent the homeserver. XCTAssertEqual(viewData.address, "http://localhost:8008", "The displayed address should match address supplied by the user, complete with the scheme.") - XCTAssertEqual(viewData.isMatrixDotOrg, false, "The server should not be detected as matrix.org.") XCTAssertTrue(viewData.showLoginForm, "The login form should be shown.") XCTAssertEqual(viewData.ssoIdentityProviders, [], "There shouldn't be any sso identity providers.") XCTAssertTrue(viewData.showRegistrationForm, "The registration form should be shown.") diff --git a/RiotTests/OnboardingTests.swift b/RiotTests/OnboardingTests.swift index 4c43fd0bd..8597a174f 100644 --- a/RiotTests/OnboardingTests.swift +++ b/RiotTests/OnboardingTests.swift @@ -58,18 +58,6 @@ class OnboardingTests: XCTestCase { XCTAssertEqual(properties.useCase, .skipped) } - func testCustomServerUseCase() { - // Given an empty set of user properties - let properties = UserSessionProperties(userId: userId) - - // When storing a custom server case result - let result = OnboardingUseCaseViewModelResult.customServer - properties.useCase = result.userSessionPropertyValue - - // Then the use case property should return nil - XCTAssertNil(properties.useCase) - } - func testUseCaseAfterDeletingProperties() { // Given a set of user properties with the Work Messaging use case let properties = UserSessionProperties(userId: userId) From 76ceba2ab7640295541129f234c73471b694eb0d Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 15 Jul 2022 14:26:16 +0100 Subject: [PATCH 13/42] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 35bf08ea9..d15503f1b 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -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 From f313a0927fa847058a34165e7f8961a0155d88a4 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 18 Jul 2022 09:44:02 +0100 Subject: [PATCH 14/42] Convert verification request and transaction to protocols --- Riot/Modules/Application/LegacyAppDelegate.h | 2 +- Riot/Modules/Application/LegacyAppDelegate.m | 10 +++++----- Riot/Modules/Room/DataSources/RoomDataSource.m | 8 ++++---- changelog.d/pr-6444.misc | 1 + 4 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 changelog.d/pr-6444.misc diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 6a6d51e4d..429fccb14 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -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)incomingKeyVerificationRequest inSession:(MXSession*)session; - (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession; diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a5fe2bd43..8bb9c9187 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -3636,11 +3636,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return; } - [mxSession.crypto.keyVerificationManager transactions:^(NSArray * _Nonnull transactions) { + [mxSession.crypto.keyVerificationManager transactions:^(NSArray> * _Nonnull transactions) { MXLogDebug(@"[AppDelegate][MXKeyVerification] checkPendingIncomingKeyVerificationsInSession: transactions: %@", transactions); - for (MXKeyVerificationTransaction *transaction in transactions) + for (id transaction in transactions) { if (transaction.isIncoming) { @@ -3664,7 +3664,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (BOOL)presentIncomingKeyVerificationRequest:(MXKeyVerificationRequest*)incomingKeyVerificationRequest +- (BOOL)presentIncomingKeyVerificationRequest:(id)incomingKeyVerificationRequest inSession:(MXSession*)session { BOOL presented = NO; @@ -3810,7 +3810,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni NSDictionary *userInfo = notification.userInfo; - MXKeyVerificationRequest *keyVerificationRequest = userInfo[MXKeyVerificationManagerNotificationRequestKey]; + id 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)keyVerificationRequest { if (keyVerificationRequest.state != MXKeyVerificationRequestStatePending) { diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index e09280b6e..8fa231159 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -678,7 +678,7 @@ const CGFloat kTypingCellHeight = 24; return roomBubbleCellData; } -- (MXKeyVerificationRequest*)keyVerificationRequestFromEventId:(NSString*)eventId +- (id)keyVerificationRequestFromEventId:(NSString*)eventId { RoomBubbleCellData *roomBubbleCellData = [self roomBubbleCellDataForEventId:eventId]; @@ -745,7 +745,7 @@ const CGFloat kTypingCellHeight = 24; queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { - MXKeyVerificationTransaction *keyVerificationTransaction = (MXKeyVerificationTransaction*)notification.object; + id keyVerificationTransaction = (id)notification.object; if ([keyVerificationTransaction.dmRoomId isEqualToString:self.roomId]) { @@ -927,7 +927,7 @@ const CGFloat kTypingCellHeight = 24; - (void)acceptVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure { - MXKeyVerificationRequest *keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId]; + id keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId]; if (!keyVerificationRequest) { @@ -950,7 +950,7 @@ const CGFloat kTypingCellHeight = 24; - (void)declineVerificationRequestForEventId:(NSString*)eventId success:(void(^)(void))success failure:(void(^)(NSError*))failure { - MXKeyVerificationRequest *keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId]; + id keyVerificationRequest = [self keyVerificationRequestFromEventId:eventId]; if (!keyVerificationRequest) { diff --git a/changelog.d/pr-6444.misc b/changelog.d/pr-6444.misc new file mode 100644 index 000000000..5f9a6f1dc --- /dev/null +++ b/changelog.d/pr-6444.misc @@ -0,0 +1 @@ +Crypto: Convert verification request and transaction to protocols From 6ee6f5585e6ea9368c0d1d58530d12098d8a2a50 Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Mon, 18 Jul 2022 10:33:06 +0100 Subject: [PATCH 15/42] Update issue automation for the design team The design team would like to see all of their issues on their board for triage now --- .github/workflows/triage-move-labelled.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index e5799dc61..f45257f26 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -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 From db2891b4cacc0767c631ad846a7eac30ce4387f2 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 15 Jul 2022 18:21:30 +0100 Subject: [PATCH 16/42] Make deep links update AuthenticationViewController before it gets shown. --- .../Legacy/LegacyAuthenticationCoordinator.swift | 11 ++++++++++- changelog.d/6425.bugfix | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 changelog.d/6425.bugfix diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index cc717dcaa..13b776c4e 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -71,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 } diff --git a/changelog.d/6425.bugfix b/changelog.d/6425.bugfix new file mode 100644 index 000000000..c17ccbe62 --- /dev/null +++ b/changelog.d/6425.bugfix @@ -0,0 +1 @@ +AuthenticationViewController is now correctly configured for a deep link if the link is opened before the view gets shown. From d1f71d76c281cce5b10c24eadeb67c981f4e346b Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 15:45:37 +0200 Subject: [PATCH 17/42] Live location sharing: Update timeline reply string. --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 399f23632..d30598320 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2520,7 +2520,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" = "has shared their live location."; +"message_reply_to_sender_sent_their_live_location" = "Live location."; "message_reply_to_message_to_reply_to_prefix" = "In reply to"; // Room members diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 31326f972..86b87d7dc 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3415,7 +3415,7 @@ public class VectorL10n: NSObject { public static var messageReplyToSenderSentAnImage: String { return VectorL10n.tr("Vector", "message_reply_to_sender_sent_an_image") } - /// has shared their live location. + /// Live location. public static var messageReplyToSenderSentTheirLiveLocation: String { return VectorL10n.tr("Vector", "message_reply_to_sender_sent_their_live_location") } From 8246edf604ab0657749677209267d2e7a58ed665 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 15:47:05 +0200 Subject: [PATCH 18/42] RoomVC: Updated supported context menu actions for beacon info event. --- Riot/Modules/Room/RoomViewController.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 26dd78547..cf4b80b8c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -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: From f7e349307afd3eff107e816641ab30a454c98e18 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 15:48:58 +0200 Subject: [PATCH 19/42] LocationPlainCell: Support long press for context menu display. --- .../Plain/Cells/Location/LocationPlainCell.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 8ddcf39fe..1fe3eb738 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -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 { From d9511a7e56cfc6b651cdddb0fa8c6e4645662ab6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 16:53:37 +0200 Subject: [PATCH 20/42] Update changes --- changelog.d/6423.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6423.change diff --git a/changelog.d/6423.change b/changelog.d/6423.change new file mode 100644 index 000000000..f09c38bc4 --- /dev/null +++ b/changelog.d/6423.change @@ -0,0 +1 @@ +Location sharing: Handle live location sharing start event reply in the timeline. From 0442342fc5f748b4535d0ed8b1ca1e75706b00ef Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 17:40:01 +0100 Subject: [PATCH 21/42] Speed up UI tests - Disable animations. - Search for screens instead of scrolling them into view. --- .../Modules/Common/Mock/ScreenList.swift | 55 ++++++++++++++----- .../Common/Test/UI/MockScreenTest.swift | 1 + .../Common/Test/UI/XCUIApplication+Riot.swift | 10 +++- RiotSwiftUI/RiotSwiftUIApp.swift | 14 ++++- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index c8c442625..6f7f221b3 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -18,34 +18,59 @@ import SwiftUI struct ScreenList: View { - private var allStates: [ScreenStateInfo] + private let allStates: [ScreenStateInfo] + + @State private var searchQuery = "" + @State private var filteredStates: [ScreenStateInfo] init(screens: [MockScreenState.Type]) { - allStates = screens - .map({ $0.stateRenderer }) - .flatMap{( $0.states )} + let states = screens + .map { $0.stateRenderer } + .flatMap { $0.states } + + allStates = states + filteredStates = states } var body: some View { NavigationView { - List { - SwiftUI.Section { - ForEach(0.. Bool { + if ProcessInfo.processInfo.environment["IS_RUNNING_UI_TESTS"] == "1" { + UIView.setAnimationsEnabled(false) + } + + return true + } +} From e6ea10f3a6420b057e74fb83904b7f7785ce0deb Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 14 Jul 2022 17:49:49 +0100 Subject: [PATCH 22/42] Prefix `test` in-front of UI test methods. --- .../Test/UI/AnalyticsPromptUITests.swift | 54 +++----- .../AuthenticationChoosePasswordUITests.swift | 41 ++---- .../AuthenticationForgotPasswordUITests.swift | 35 ++--- .../Test/UI/AuthenticationLoginUITests.swift | 69 +++++----- .../UI/AuthenticationReCaptchaUITests.swift | 2 +- .../AuthenticationRegistrationUITests.swift | 127 ++++++++++-------- ...AuthenticationServerSelectionUITests.swift | 47 +++---- .../UI/AuthenticationSoftLogoutUITests.swift | 53 +++----- .../Test/UI/AuthenticationTermsUITests.swift | 30 ++--- .../UI/AuthenticationVerifyEmailUITests.swift | 35 ++--- .../AuthenticationVerifyMsisdnUITests.swift | 41 ++---- .../Common/Test/UI/MockScreenTest.swift | 51 +------ .../UI/LiveLocationLabPromotionUITests.swift | 2 +- .../Test/UI/OnboardingAvatarUITests.swift | 30 ++--- .../UI/OnboardingCelebrationUITests.swift | 2 +- .../UI/OnboardingCongratulationsUITests.swift | 29 +--- .../UI/OnboardingDisplayNameUITests.swift | 39 ++---- .../Test/UI/OnboardingUseCaseUITests.swift | 2 +- .../UI/LiveLocationSharingViewerUITests.swift | 7 +- .../Test/UI/LocationSharingUITests.swift | 12 +- .../Test/UI/PollEditFormUITests.swift | 15 +-- .../UI/RoomAccessTypeChooserUITests.swift | 2 +- .../Test/UI/RoomUpgradeUITests.swift | 2 +- .../UI/StaticLocationViewingUITests.swift | 26 +--- .../Test/UI/TimelinePollUITests.swift | 12 +- .../Test/UI/UserSuggestionUITests.swift | 17 +-- .../Test/UI/ChangePasswordUITests.swift | 41 ++---- .../Test/UI/MatrixItemChooserUITests.swift | 37 ++--- .../UI/SpaceCreationEmailInvitesUITests.swift | 46 +++---- .../Test/UI/SpaceCreationMenuUITests.swift | 24 +--- .../UI/SpaceCreationPostProcessUITests.swift | 19 +-- .../Test/UI/SpaceCreationRoomsUITests.swift | 28 ++-- .../UI/SpaceCreationSettingsUITests.swift | 30 ++--- .../Test/UI/SpaceSettingsUITests.swift | 2 +- .../Test/UI/TemplateSimpleScreenUITests.swift | 34 ++--- .../Test/UI/TemplateUserProfileUITests.swift | 45 ++++--- .../Test/UI/TemplateRoomChatUITests.swift | 43 ++---- .../Test/UI/TemplateRoomListUITests.swift | 29 +--- changelog.d/6432.build | 1 + 39 files changed, 404 insertions(+), 757 deletions(-) create mode 100644 changelog.d/6432.build diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index fc12e4e74..2013f0866 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -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) } - } diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift index c4e7f564a..d1fffa0fb 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift @@ -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.") diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift index 06b1948f4..06478ce2a 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift @@ -17,29 +17,10 @@ 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.") @@ -66,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.") @@ -92,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.") diff --git a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift index 7824a24fa..8dcf6eb63 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift @@ -17,40 +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)) +class AuthenticationLoginUITests: MockScreenTestCase { + func testMatrixDotOrg() { + app.goToScreenWithIdentifier(MockAuthenticationLoginScreenState.matrixDotOrg.title) + + let state = "matrix.org" + validateLoginFormIsVisible(for: state) + validateSSOButtonsAreShown(for: state) } - func verifyAuthenticationLoginScreen() throws { - guard let screenState = screenState as? MockAuthenticationLoginScreenState else { fatalError("no screen") } - switch screenState { - case .matrixDotOrg: - let state = "matrix.org" - validateLoginFormIsVisible(for: state) - validateSSOButtonsAreShown(for: state) - case .passwordOnly: - let state = "a password only server" - 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" - validateLoginFormIsHidden(for: state) - validateSSOButtonsAreShown(for: state) - case .fallback: - let state = "a fallback server" - validateFallback(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. diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift index bd198bf86..ac8ff628c 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift @@ -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. } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift index a94ee11df..223d20c40 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift @@ -17,69 +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) - - validateUnknownUsernameAvailability(for: state) - validateNoPasswordErrorsAreShown(for: state) - case .passwordOnly: - let state = "a password only server" - validateRegistrationFormIsVisible(for: state) - validateSSOButtonsAreHidden(for: state) - validateFallbackButtonIsHidden(for: state) - - validateNextButtonIsDisabled(for: state) - - validateUnknownUsernameAvailability(for: state) - validateNoPasswordErrorsAreShown(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) - - validateUsernameAvailable(for: state) - validateNoPasswordErrorsAreShown(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 diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift index 53f21e0d7..e72e00ec4 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift @@ -17,33 +17,10 @@ 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: - verifyRegisterState() - case .login: - verifyLoginState() - case .emptyAddress: - verifyEmptyAddress() - case .invalidAddress: - verifyInvalidAddress() - case .nonModal: - verifyNonModalPresentation() - } - } - - func verifyRegisterState() { +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"] @@ -65,14 +42,18 @@ class AuthenticationServerSelectionUITests: MockScreenTest { } - func verifyLoginState() { + 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 verifyEmptyAddress() { + 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.") @@ -81,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.") @@ -94,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.") diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift index 52bd56ca5..52268610a 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift @@ -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.") } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift b/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift index f7ea429eb..3cb31c36a 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift @@ -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) { diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift index c80a0cdb8..73706abcd 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift @@ -17,29 +17,10 @@ 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.") @@ -60,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.") @@ -80,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.") diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift index a9fddacb8..f5528acfa 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift @@ -17,31 +17,10 @@ import XCTest import RiotSwiftUI -class AuthenticationVerifyMsisdnUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockAuthenticationVerifyMsisdnScreenState.self - } - - override class func createTest() -> MockScreenTest { - return AuthenticationVerifyMsisdnUITests(selector: #selector(verifyAuthenticationVerifyMsisdnScreen)) - } - - func verifyAuthenticationVerifyMsisdnScreen() throws { - guard let screenState = screenState as? MockAuthenticationVerifyMsisdnScreenState else { fatalError("no screen") } - switch screenState { - case .emptyPhoneNumber: - verifyEmptyPhoneNumber() - case .enteredPhoneNumber: - verifyEnteredPhoneNumber() - case .hasSentSMS: - verifyHasSentSMS() - case .enteredOTP: - verifyEnteredOTP() - } - } - - func verifyEmptyPhoneNumber() { +class AuthenticationVerifyMsisdnUITests: MockScreenTestCase { + func testEmptyPhoneNumber() { + app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.emptyPhoneNumber.title) + let titleLabel = app.staticTexts["titleLabel"] XCTAssertTrue(titleLabel.exists, "The title should be shown.") @@ -65,7 +44,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest { XCTAssertEqual(cancelButton.label, "Cancel") } - func verifyEnteredPhoneNumber() { + func testEnteredPhoneNumber() { + app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.enteredPhoneNumber.title) + let titleLabel = app.staticTexts["titleLabel"] XCTAssertTrue(titleLabel.exists, "The title should be shown.") @@ -88,7 +69,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest { XCTAssertEqual(cancelButton.label, "Cancel") } - func verifyHasSentSMS() { + func testHasSentSMS() { + app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.hasSentSMS.title) + let titleLabel = app.staticTexts["titleLabel"] XCTAssertTrue(titleLabel.exists, "The title should be shown.") @@ -116,7 +99,9 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTest { XCTAssertEqual(backButton.label, "Back") } - func verifyEnteredOTP() { + func testEnteredOTP() { + app.goToScreenWithIdentifier(MockAuthenticationVerifyMsisdnScreenState.enteredOTP.title) + let titleLabel = app.staticTexts["titleLabel"] XCTAssertTrue(titleLabel.exists, "The title should be shown.") diff --git a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift index 583b6099c..3c00337d1 100644 --- a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift +++ b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift @@ -18,55 +18,16 @@ import XCTest import RiotSwiftUI /// XCTestCase subclass to ease testing of `MockScreenState`. -/// Creates a test case for each screen state, launches the app, -/// goes to the correct screen and provides the state and key for each -/// invocation of the test. -class MockScreenTest: XCTestCase { - - enum Constants { - static let defaultTimeout: TimeInterval = 3 - } - - class var screenType: MockScreenState.Type? { - return nil - } - - class func createTest() -> MockScreenTest { - return MockScreenTest() - } - - var screenState: MockScreenState? - var screenStateKey: String? +/// Launches the app with an environment variable used to disable animations. +/// Begin each test with the following code before checking the UI: +/// ``` +/// app.goToScreenWithIdentifier(MockTemplateScreenState.someScreenState.title) +/// ``` +class MockScreenTestCase: XCTestCase { let app = XCUIApplication() - - override class var defaultTestSuite: XCTestSuite { - let testSuite = XCTestSuite(name: NSStringFromClass(self)) - guard let screenType = screenType else { - return testSuite - } - - // Create a test case for each screen state - screenType.screenStates.enumerated().forEach { index, screenState in - let key = screenType.screenNames[index] - addTestFor(screenState: screenState, screenStateKey: key, toTestSuite: testSuite) - } - return testSuite - } - - class func addTestFor(screenState: MockScreenState, screenStateKey: String, toTestSuite testSuite: XCTestSuite) { - let test = createTest() - test.screenState = screenState - test.screenStateKey = screenStateKey - testSuite.addTest(test) - } open override func setUpWithError() throws { - // For every test case launch the app and go to the relevant screen - continueAfterFailure = false app.launchEnvironment = ["IS_RUNNING_UI_TESTS": "1"] app.launch() - - guard let screenKey = screenStateKey else { fatalError("no screen") } - app.goToScreenWithIdentifier(screenKey) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift index 20fe9f0a5..4b71228fc 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class LiveLocationLabPromotionUITests: MockScreenTest { +class LiveLocationLabPromotionUITests: MockScreenTestCase { // Nothing to test as the view is completely static } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift index 762728dd5..dc71847b5 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift @@ -17,27 +17,13 @@ import XCTest import RiotSwiftUI -class OnboardingAvatarUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockOnboardingAvatarScreenState.self - } - - override class func createTest() -> MockScreenTest { - return OnboardingAvatarUITests(selector: #selector(verifyOnboardingAvatarScreen)) - } - - func verifyOnboardingAvatarScreen() throws { - guard let screenState = screenState as? MockOnboardingAvatarScreenState else { fatalError("no screen") } - switch screenState { - case .placeholderAvatar(let userId, let displayName): - verifyPlaceholderAvatar(userId: userId, displayName: displayName) - case .userSelectedAvatar: - verifyUserSelectedAvatar() - } - } +class OnboardingAvatarUITests: MockScreenTestCase { + let userId = "@example:matrix.org" + let displayName = "Jane" - func verifyPlaceholderAvatar(userId: String, displayName: String) { + func testPlaceholderAvatar() { + app.goToScreenWithIdentifier(MockOnboardingAvatarScreenState.placeholderAvatar(userId: userId, displayName: displayName).title) + guard let firstLetter = displayName.uppercased().first else { XCTFail("Unable to get the first letter of the display name.") return @@ -55,7 +41,9 @@ class OnboardingAvatarUITests: MockScreenTest { XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.") } - func verifyUserSelectedAvatar() { + func testUserSelectedAvatar() { + app.goToScreenWithIdentifier(MockOnboardingAvatarScreenState.userSelectedAvatar(userId: userId, displayName: displayName).title) + let placeholderAvatar = app.otherElements["placeholderAvatar"] XCTAssertFalse(placeholderAvatar.exists, "The placeholder avatar should be hidden.") diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift index 27de0a119..12d0f210a 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class OnboardingCelebrationUITests: MockScreenTest { +class OnboardingCelebrationUITests: MockScreenTestCase { // Nothing to test as the view is completely static } diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift index 43ccaffb8..9b4c8f9f6 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift @@ -17,27 +17,10 @@ import XCTest import RiotSwiftUI -class OnboardingCongratulationsUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockOnboardingCongratulationsScreenState.self - } - - override class func createTest() -> MockScreenTest { - return OnboardingCongratulationsUITests(selector: #selector(verifyOnboardingCongratulationsScreen)) - } - - func verifyOnboardingCongratulationsScreen() throws { - guard let screenState = screenState as? MockOnboardingCongratulationsScreenState else { fatalError("no screen") } - switch screenState { - case .regular: - verifyButtons() - case .personalizationDisabled: - verifyButtonsWhenPersonalizationIsDisabled() - } - } - - func verifyButtons() { +class OnboardingCongratulationsUITests: MockScreenTestCase { + func testButtons() { + app.goToScreenWithIdentifier(MockOnboardingCongratulationsScreenState.regular.title) + let personalizeButton = app.buttons["personalizeButton"] XCTAssertTrue(personalizeButton.exists, "The personalization button should be shown.") @@ -45,7 +28,9 @@ class OnboardingCongratulationsUITests: MockScreenTest { XCTAssertTrue(homeButton.exists, "The home button should always be shown.") } - func verifyButtonsWhenPersonalizationIsDisabled() { + func testButtonsWhenPersonalizationIsDisabled() { + app.goToScreenWithIdentifier(MockOnboardingCongratulationsScreenState.personalizationDisabled.title) + let personalizeButton = app.buttons["personalizeButton"] XCTAssertFalse(personalizeButton.exists, "The personalization button should be hidden.") diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift index 177be76e3..6c27e09de 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift @@ -17,29 +17,10 @@ import XCTest import RiotSwiftUI -class OnboardingDisplayNameUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockOnboardingDisplayNameScreenState.self - } - - override class func createTest() -> MockScreenTest { - return OnboardingDisplayNameUITests(selector: #selector(verifyOnboardingDisplayNameScreen)) - } - - func verifyOnboardingDisplayNameScreen() throws { - guard let screenState = screenState as? MockOnboardingDisplayNameScreenState else { fatalError("no screen") } - switch screenState { - case .emptyTextField: - verifyEmptyTextField() - case .filledTextField(let displayName): - verifyDisplayName(displayName: displayName) - case .longDisplayName(displayName: let displayName): - verifyLongDisplayName(displayName: displayName) - } - } - - func verifyEmptyTextField() { +class OnboardingDisplayNameUITests: MockScreenTestCase { + func testEmptyTextField() { + app.goToScreenWithIdentifier(MockOnboardingDisplayNameScreenState.emptyTextField.title) + let textField = app.textFields.element XCTAssertTrue(textField.exists, "The textfield should always be shown.") XCTAssertEqual(textField.value as? String, VectorL10n.onboardingDisplayNamePlaceholder, "When the textfield is empty, the value should match the placeholder.") @@ -54,7 +35,10 @@ class OnboardingDisplayNameUITests: MockScreenTest { XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.") } - func verifyDisplayName(displayName: String) { + func testDisplayName() { + let displayName = "Test User" + app.goToScreenWithIdentifier(MockOnboardingDisplayNameScreenState.filledTextField(displayName: displayName).title) + let textField = app.textFields.element XCTAssertTrue(textField.exists, "The textfield should always be shown.") XCTAssertEqual(textField.value as? String, displayName, "When a name has been set, it should show in the textfield.") @@ -69,7 +53,12 @@ class OnboardingDisplayNameUITests: MockScreenTest { XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameHint, "The footer should display a hint when an acceptable name is entered.") } - func verifyLongDisplayName(displayName: String) { + func testLongDisplayName() { + let displayName = """ + Bacon ipsum dolor amet filet mignon chicken kevin andouille. Doner shoulder beef, brisket bresaola turkey jowl venison. Ham hock cow turducken, chislic venison doner short loin strip steak tri-tip jowl. Sirloin pork belly hamburger ribeye. Tail capicola alcatra short ribs turkey doner. + """ + app.goToScreenWithIdentifier(MockOnboardingDisplayNameScreenState.longDisplayName(displayName: displayName).title) + let textField = app.textFields.element XCTAssertTrue(textField.exists, "The textfield should always be shown.") XCTAssertEqual(textField.value as? String, displayName, "When a name has been set, it should show in the textfield.") diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift index 929e6e328..fbb0a9a64 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class OnboardingUseCaseUITests: MockScreenTest { +class OnboardingUseCaseUITests: MockScreenTestCase { // The view has no parameters or changing state to test. } diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift index f3e032d7c..6d4333d4c 100644 --- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift @@ -17,9 +17,6 @@ import XCTest import RiotSwiftUI -class LiveLocationSharingViewerUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockLiveLocationSharingViewerScreenState.self - } +class LiveLocationSharingViewerUITests: MockScreenTestCase { + // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift index 65029026c..d9fb19faa 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -17,17 +17,7 @@ import XCTest import RiotSwiftUI -class LocationSharingUITests: XCTestCase { - - private var app: XCUIApplication! - - override func setUp() { - continueAfterFailure = false - - app = XCUIApplication() - app.launch() - } - +class LocationSharingUITests: MockScreenTestCase { func testInitialUserLocation() { goToScreenWithIdentifier(MockLocationSharingScreenState.shareUserLocation.title) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift index 2ce4ca26c..d56c88391 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -17,19 +17,9 @@ import XCTest import RiotSwiftUI -class PollEditFormUITests: XCTestCase { - - private var app: XCUIApplication! - - override func setUp() { - continueAfterFailure = false - - app = XCUIApplication() - app.launch() - app.goToScreenWithIdentifier(MockPollEditFormScreenState.standard.title) - } - +class PollEditFormUITests: MockScreenTestCase { func testInitialStateComponents() { + app.goToScreenWithIdentifier(MockPollEditFormScreenState.standard.title) XCTAssert(app.scrollViews.firstMatch.exists) @@ -58,6 +48,7 @@ class PollEditFormUITests: XCTestCase { } func testRemoveAddAnswerOptions() { + app.goToScreenWithIdentifier(MockPollEditFormScreenState.standard.title) let deleteAnswerOptionButton = app.buttons["Delete answer option"].firstMatch diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift index 0e51a82d4..d6f5b3dbb 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class RoomAccessTypeChooserUITests: MockScreenTest { +class RoomAccessTypeChooserUITests: MockScreenTestCase { // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift index e78261aea..5b59c1c5d 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class RoomUpgradeUITests: MockScreenTest { +class RoomUpgradeUITests: MockScreenTestCase { // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift index d459b6df3..d6f26f247 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift @@ -17,28 +17,6 @@ import XCTest import RiotSwiftUI -class StaticLocationViewingUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockStaticLocationViewingScreenState.self - } - - override class func createTest() -> MockScreenTest { - return StaticLocationViewingUITests(selector: #selector(verifyStaticLocationViewingScreen)) - } - - func verifyStaticLocationViewingScreen() { - guard let screenState = screenState as? MockStaticLocationViewingScreenState else { fatalError("no screen") } - - switch screenState { - case .showUserLocation: - verifyInitialExistingLocation() - case .showPinLocation: - verifyInitialExistingLocation() - } - } - - func verifyInitialExistingLocation() { - // This test has issues running consistently on CI. Removed for now until the issue has been fixed. - } +class StaticLocationViewingUITests: MockScreenTestCase { + // This test has issues running consistently on CI. Removed for now until the issue has been fixed. } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift index 0cf4d5071..98b520ea7 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift @@ -17,17 +17,7 @@ import XCTest import RiotSwiftUI -class TimelinePollUITests: XCTestCase { - - private var app: XCUIApplication! - - override func setUp() { - continueAfterFailure = false - - app = XCUIApplication() - app.launch() - } - +class TimelinePollUITests: MockScreenTestCase { func testOpenDisclosedPoll() { app.goToScreenWithIdentifier(MockTimelinePollScreenState.openDisclosed.title) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index 8b5af53c0..53e3c883d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -17,18 +17,11 @@ import XCTest import RiotSwiftUI -class UserSuggestionUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockUserSuggestionScreenState.self - } - - override class func createTest() -> MockScreenTest { - return UserSuggestionUITests(selector: #selector(verifyUserSuggestionScreen)) - } - - func verifyUserSuggestionScreen() throws { - XCTAssert(app.tables.firstMatch.exists) +class UserSuggestionUITests: MockScreenTestCase { + func testUserSuggestionScreen() throws { + app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title) + + XCTAssert(app.tables.firstMatch.waitForExistence(timeout: 1)) let firstButton = app.tables.firstMatch.buttons.firstMatch _ = firstButton.waitForExistence(timeout: 10) diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift index d534eac16..c96ef6f27 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift @@ -17,31 +17,10 @@ import XCTest import RiotSwiftUI -class ChangePasswordUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockChangePasswordScreenState.self - } - - override class func createTest() -> MockScreenTest { - return ChangePasswordUITests(selector: #selector(verifyChangePasswordScreen)) - } - - func verifyChangePasswordScreen() throws { - guard let screenState = screenState as? MockChangePasswordScreenState else { fatalError("no screen") } - switch screenState { - case .allEmpty: - verifyAllEmpty() - case .cannotSubmit: - verifyCannotSubmit() - case .canSubmit: - verifyCanSubmit() - case .canSubmitAndSignoutAllDevicesChecked: - verifyCanSubmitAndSignoutAllDevicesChecked() - } - } - - func verifyAllEmpty() { +class ChangePasswordUITests: MockScreenTestCase { + func testAllEmpty() { + app.goToScreenWithIdentifier(MockChangePasswordScreenState.allEmpty.title) + XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.") XCTAssertTrue(app.staticTexts["passwordRequirementsLabel"].exists, "The password requirements label should be shown.") @@ -66,7 +45,9 @@ class ChangePasswordUITests: MockScreenTest { XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked") } - func verifyCannotSubmit() { + func testCannotSubmit() { + app.goToScreenWithIdentifier(MockChangePasswordScreenState.cannotSubmit.title) + XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.") XCTAssertTrue(app.staticTexts["passwordRequirementsLabel"].exists, "The password requirements label should be shown.") @@ -91,7 +72,9 @@ class ChangePasswordUITests: MockScreenTest { XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked") } - func verifyCanSubmit() { + func testCanSubmit() { + app.goToScreenWithIdentifier(MockChangePasswordScreenState.canSubmit.title) + XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.") XCTAssertTrue(app.staticTexts["passwordRequirementsLabel"].exists, "The password requirements label should be shown.") @@ -116,7 +99,9 @@ class ChangePasswordUITests: MockScreenTest { XCTAssertFalse(signoutAllDevicesToggle.isOn, "Sign out all devices should be unchecked") } - func verifyCanSubmitAndSignoutAllDevicesChecked() { + func testCanSubmitAndSignoutAllDevicesChecked() { + app.goToScreenWithIdentifier(MockChangePasswordScreenState.canSubmitAndSignoutAllDevicesChecked.title) + XCTAssertTrue(app.staticTexts["titleLabel"].exists, "The title should be shown.") XCTAssertTrue(app.staticTexts["passwordRequirementsLabel"].exists, "The password requirements label should be shown.") diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift index c5e8f305b..edcd683a3 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift @@ -17,44 +17,27 @@ import XCTest import RiotSwiftUI -class MatrixItemChooserUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockMatrixItemChooserScreenState.self - } - - override class func createTest() -> MockScreenTest { - return MatrixItemChooserUITests(selector: #selector(verifyMatrixItemChooserScreen)) - } - - func verifyMatrixItemChooserScreen() throws { - guard let screenState = screenState as? MockMatrixItemChooserScreenState else { fatalError("no screen") } - switch screenState { - case .noItems: - verifyEmptyScreen() - case .items: - verifyPopulatedScreen() - case .selectedItems: - verifyPopulatedWithSelectionScreen() - case .selectionHeader: - break - } - } - - func verifyEmptyScreen() { +class MatrixItemChooserUITests: MockScreenTestCase { + func testEmptyScreen() { + app.goToScreenWithIdentifier(MockMatrixItemChooserScreenState.noItems.title) + XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, true) XCTAssertEqual(app.staticTexts["emptyListMessage"].label, VectorL10n.spacesNoResultFoundTitle) } - func verifyPopulatedScreen() { + func testPopulatedScreen() { + app.goToScreenWithIdentifier(MockMatrixItemChooserScreenState.items.title) + XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) } - func verifyPopulatedWithSelectionScreen() { + func testPopulatedWithSelectionScreen() { + app.goToScreenWithIdentifier(MockMatrixItemChooserScreenState.selectedItems.title) + XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift index 2b680e2aa..897230312 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift @@ -19,31 +19,31 @@ import XCTest import RiotSwiftUI -class SpaceCreationEmailInvitesUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceCreationEmailInvitesScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceCreationEmailInvitesUITests(selector: #selector(verifySpaceCreationEmailInvitesScreen)) - } - - func verifySpaceCreationEmailInvitesScreen() throws { - guard let screenState = screenState as? MockSpaceCreationEmailInvitesScreenState else { fatalError("no screen") } - switch screenState { - case .defaultEmailValues: - verifyEmailValues() - case .emailEntered: - verifyEmailValues() - case .emailValidationFailed: - verifyEmailValues() - case .loading: - verifyEmailValues() - } +class SpaceCreationEmailInvitesUITests: MockScreenTestCase { + func testDefaultEmailValues() { + app.goToScreenWithIdentifier(MockSpaceCreationEmailInvitesScreenState.defaultEmailValues.title) + + let emailTextFieldsCount = app.textFields.matching(identifier: "emailTextField").count + XCTAssertEqual(emailTextFieldsCount, 2) } - func verifyEmailValues() { + func testEmailEntered() { + app.goToScreenWithIdentifier(MockSpaceCreationEmailInvitesScreenState.emailEntered.title) + + let emailTextFieldsCount = app.textFields.matching(identifier: "emailTextField").count + XCTAssertEqual(emailTextFieldsCount, 2) + } + + func testEmailValidationFailed() { + app.goToScreenWithIdentifier(MockSpaceCreationEmailInvitesScreenState.emailValidationFailed.title) + + let emailTextFieldsCount = app.textFields.matching(identifier: "emailTextField").count + XCTAssertEqual(emailTextFieldsCount, 2) + } + + func testLoading() { + app.goToScreenWithIdentifier(MockSpaceCreationEmailInvitesScreenState.loading.title) + let emailTextFieldsCount = app.textFields.matching(identifier: "emailTextField").count XCTAssertEqual(emailTextFieldsCount, 2) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift index c2f457f79..251fb5429 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift @@ -19,25 +19,10 @@ import XCTest import RiotSwiftUI -class SpaceCreationMenuUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceCreationMenuScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceCreationMenuUITests(selector: #selector(verifySpaceCreationMenuScreen)) - } - - func verifySpaceCreationMenuScreen() throws { - guard let screenState = screenState as? MockSpaceCreationMenuScreenState else { fatalError("no screen") } - switch screenState { - case .options: - verifySpaceCreationMenuOptions() - } - } - - func verifySpaceCreationMenuOptions() { +class SpaceCreationMenuUITests: MockScreenTestCase { + func testSpaceCreationMenuOptions() { + app.goToScreenWithIdentifier(MockSpaceCreationMenuScreenState.options.title) + let optionButtonCount = app.buttons.matching(identifier:"optionButton").count XCTAssertEqual(optionButtonCount, 2) @@ -49,5 +34,4 @@ class SpaceCreationMenuUITests: MockScreenTest { XCTAssert(detailText.exists) XCTAssertEqual(detailText.label, "Some detail text") } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift index e453d6436..75c624772 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift @@ -19,21 +19,6 @@ import XCTest import RiotSwiftUI -class SpaceCreationPostProcessUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceCreationPostProcessScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceCreationPostProcessUITests(selector: #selector(verifySpaceCreationPostProcessScreen)) - } - - func verifySpaceCreationPostProcessScreen() throws { - guard let screenState = screenState as? MockSpaceCreationPostProcessScreenState else { fatalError("no screen") } - } - - func verifyTasksList() { - } - +class SpaceCreationPostProcessUITests: MockScreenTestCase { + // No tests have been implemented } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift index 2d9d633c1..2dedafb37 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift @@ -19,27 +19,17 @@ import XCTest import RiotSwiftUI -class SpaceCreationRoomsUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceCreationRoomsScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceCreationRoomsUITests(selector: #selector(verifySpaceCreationRoomsScreen)) - } - - func verifySpaceCreationRoomsScreen() throws { - guard let screenState = screenState as? MockSpaceCreationRoomsScreenState else { fatalError("no screen") } - switch screenState { - case .defaultValues: - verifyValueTextFields() - case .valuesEntered: - verifyValueTextFields() - } +class SpaceCreationRoomsUITests: MockScreenTestCase { + func testDefaultValues() { + app.goToScreenWithIdentifier(MockSpaceCreationRoomsScreenState.defaultValues.title) + + let emailTextFieldsCount = app.textFields.matching(identifier: "roomTextField").count + XCTAssertEqual(emailTextFieldsCount, 3) } - func verifyValueTextFields() { + func testValuesEntered() { + app.goToScreenWithIdentifier(MockSpaceCreationRoomsScreenState.valuesEntered.title) + let emailTextFieldsCount = app.textFields.matching(identifier: "roomTextField").count XCTAssertEqual(emailTextFieldsCount, 3) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift index 9d6dfc4c7..f1f0e705d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift @@ -19,32 +19,18 @@ import XCTest import RiotSwiftUI -class SpaceCreationSettingsUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceCreationSettingsScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceCreationSettingsUITests(selector: #selector(verifySpaceCreationSettingsScreen)) - } - - func verifySpaceCreationSettingsScreen() throws { - guard let screenState = screenState as? MockSpaceCreationSettingsScreenState else { fatalError("no screen") } - switch screenState { - case .privateSpace: break - case .validated: break - case .validationFailed: break - } - } - - func verifyPrivateSpace() { +class SpaceCreationSettingsUITests: MockScreenTestCase { + func testPrivateSpace() { + app.goToScreenWithIdentifier(MockSpaceCreationSettingsScreenState.privateSpace.title) + let addressTextField = app.groups["addressTextField"] XCTAssertEqual(addressTextField.exists, false) } - func verifyPublicValidated() { + func testPublicValidated() { + app.goToScreenWithIdentifier(MockSpaceCreationSettingsScreenState.validated.title) + let addressTextField = app.groups["addressTextField"] - XCTAssertEqual(addressTextField.exists, true) + XCTAssertEqual(addressTextField.exists, false) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift index bc5e3900d..abece45dd 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift @@ -17,6 +17,6 @@ import XCTest import RiotSwiftUI -class SpaceSettingsUITests: MockScreenTest { +class SpaceSettingsUITests: MockScreenTestCase { // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift index d09fc7d6b..4b02918dd 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift @@ -17,28 +17,22 @@ import XCTest import RiotSwiftUI -class TemplateSimpleScreenUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockTemplateSimpleScreenScreenState.self - } - - override class func createTest() -> MockScreenTest { - return TemplateSimpleScreenUITests(selector: #selector(verifyTemplateSimpleScreenScreen)) - } - - func verifyTemplateSimpleScreenScreen() throws { - guard let screenState = screenState as? MockTemplateSimpleScreenScreenState else { fatalError("no screen") } - switch screenState { - case .promptType(let promptType): - verifyTemplateSimpleScreenPromptType(promptType: promptType) - } - } - - func verifyTemplateSimpleScreenPromptType(promptType: TemplateSimpleScreenPromptType) { +class TemplateSimpleScreenUITests: MockScreenTestCase { + func testTemplateSimpleScreenPromptRegular() { + let promptType = TemplateSimpleScreenPromptType.regular + app.goToScreenWithIdentifier(MockTemplateSimpleScreenScreenState.promptType(promptType).title) + + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + + func testTemplateSimpleScreenPromptUpgrade() { + let promptType = TemplateSimpleScreenPromptType.upgrade + app.goToScreenWithIdentifier(MockTemplateSimpleScreenScreenState.promptType(promptType).title) + let title = app.staticTexts["title"] XCTAssert(title.exists) XCTAssertEqual(title.label, promptType.title) } - } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift index 690a3c43c..b23b4174e 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift @@ -17,33 +17,38 @@ import XCTest import RiotSwiftUI -class TemplateUserProfileUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockTemplateUserProfileScreenState.self +class TemplateUserProfileUITests: MockScreenTestCase { + func testTemplateUserProfilePresenceIdle() { + let presence = TemplateUserProfilePresence.idle + app.goToScreenWithIdentifier(MockTemplateUserProfileScreenState.presence(presence).title) + + let presenceText = app.staticTexts["presenceText"] + XCTAssert(presenceText.exists) + XCTAssertEqual(presenceText.label, presence.title) } - - override class func createTest() -> MockScreenTest { - return TemplateUserProfileUITests(selector: #selector(verifyTemplateUserProfileScreen)) + + func testTemplateUserProfilePresenceOffline() { + let presence = TemplateUserProfilePresence.offline + app.goToScreenWithIdentifier(MockTemplateUserProfileScreenState.presence(presence).title) + + let presenceText = app.staticTexts["presenceText"] + XCTAssert(presenceText.exists) + XCTAssertEqual(presenceText.label, presence.title) } - - func verifyTemplateUserProfileScreen() throws { - guard let screenState = screenState as? MockTemplateUserProfileScreenState else { fatalError("no screen") } - switch screenState { - case .presence(let presence): - verifyTemplateUserProfilePresence(presence: presence) - case .longDisplayName(let name): - verifyTemplateUserProfileLongName(name: name) - } - } - - func verifyTemplateUserProfilePresence(presence: TemplateUserProfilePresence) { + + func testTemplateUserProfilePresenceOnline() { + let presence = TemplateUserProfilePresence.online + app.goToScreenWithIdentifier(MockTemplateUserProfileScreenState.presence(presence).title) + let presenceText = app.staticTexts["presenceText"] XCTAssert(presenceText.exists) XCTAssertEqual(presenceText.label, presence.title) } - func verifyTemplateUserProfileLongName(name: String) { + func testTemplateUserProfileLongName() { + let name = "Somebody with a super long name we would like to test" + app.goToScreenWithIdentifier(MockTemplateUserProfileScreenState.longDisplayName(name).title) + let displayNameText = app.staticTexts["displayNameText"] XCTAssert(displayNameText.exists) XCTAssertEqual(displayNameText.label, name) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift index b06b509c6..449eddcc0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift @@ -17,46 +17,31 @@ import XCTest import RiotSwiftUI -class TemplateRoomChatUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockTemplateRoomChatScreenState.self - } - - override class func createTest() -> MockScreenTest { - return TemplateRoomChatUITests(selector: #selector(verifyTemplateRoomChatScreen)) - } - - func verifyTemplateRoomChatScreen() throws { - guard let screenState = screenState as? MockTemplateRoomChatScreenState else { fatalError("no screen") } - switch screenState { - case .initializingRoom: - verifyInitializingRoom() - case .failedToInitializeRoom: - verifyFailedToInitializeRoom() - case .noMessages: - verifyNoMessages() - case .messages: - verifyMessages() - } - } - - func verifyInitializingRoom() { +class TemplateRoomChatUITests: MockScreenTestCase { + func testInitializingRoom() { + app.goToScreenWithIdentifier(MockTemplateRoomChatScreenState.initializingRoom.title) + let loadingProgress = app.activityIndicators["loadingProgress"] XCTAssert(loadingProgress.exists) } - func verifyFailedToInitializeRoom() { + func testFailedToInitializeRoom() { + app.goToScreenWithIdentifier(MockTemplateRoomChatScreenState.failedToInitializeRoom.title) + let errorMessage = app.staticTexts["errorMessage"] XCTAssert(errorMessage.exists) } - func verifyNoMessages() { + func testNoMessages() { + app.goToScreenWithIdentifier(MockTemplateRoomChatScreenState.noMessages.title) + let errorMessage = app.staticTexts["errorMessage"] XCTAssert(errorMessage.exists) } - func verifyMessages() { + func testMessages() { + app.goToScreenWithIdentifier(MockTemplateRoomChatScreenState.messages.title) + // Verify bubble grouping with: // 3 bubbles let bubbleCount = app.images.matching(identifier:"bubbleImage").count @@ -65,8 +50,6 @@ class TemplateRoomChatUITests: MockScreenTest { // and 4 text items let bubbleTextItemCount = app.staticTexts.matching(identifier:"bubbleTextContent").count XCTAssertEqual(bubbleTextItemCount, 4) - - } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift index 7a7befcc3..8ee0012e2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift @@ -17,33 +17,18 @@ import XCTest import RiotSwiftUI -class TemplateRoomListUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockTemplateRoomListScreenState.self - } - - override class func createTest() -> MockScreenTest { - return TemplateRoomListUITests(selector: #selector(verifyTemplateRoomListScreen)) - } - - func verifyTemplateRoomListScreen() throws { - guard let screenState = screenState as? MockTemplateRoomListScreenState else { fatalError("no screen") } - switch screenState { - case .noRooms: - verifyTemplateRoomListNoRooms() - case .rooms: - verifyTemplateRoomListRooms() - } - } - - func verifyTemplateRoomListNoRooms() { +class TemplateRoomListUITests: MockScreenTestCase { + func testTemplateRoomListNoRooms() { + app.goToScreenWithIdentifier(MockTemplateRoomListScreenState.noRooms.title) + let errorMessage = app.staticTexts["errorMessage"] XCTAssert(errorMessage.exists) XCTAssert(errorMessage.label == "No Rooms") } - func verifyTemplateRoomListRooms() { + func testTemplateRoomListRooms() { + app.goToScreenWithIdentifier(MockTemplateRoomListScreenState.rooms.title) + let displayNameCount = app.buttons.matching(identifier:"roomNameText").count XCTAssertEqual(displayNameCount, 3) } diff --git a/changelog.d/6432.build b/changelog.d/6432.build new file mode 100644 index 000000000..36fde2b9f --- /dev/null +++ b/changelog.d/6432.build @@ -0,0 +1 @@ +Fix UI tests failing on CI but not being reported by prefixing all tests with `test`. From d3be937d6a605da78c48fd13b839ea180883016f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 17:20:26 +0200 Subject: [PATCH 23/42] RoomTimelineLocationView: Show map credits only when map is visible. --- Riot/Modules/Room/Location/RoomTimelineLocationView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 4e59799e7..d89f3c93d 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -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 From aa8ed8a38ca1c05a85cef976cf91d76bcdaff19f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 18 Jul 2022 17:22:03 +0200 Subject: [PATCH 24/42] Update changes --- changelog.d/6448.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6448.change diff --git a/changelog.d/6448.change b/changelog.d/6448.change new file mode 100644 index 000000000..11dacede8 --- /dev/null +++ b/changelog.d/6448.change @@ -0,0 +1 @@ +Location sharing: Show map credits on live location timeline tile only when map is shown. \ No newline at end of file From 6a3deb4c5c03914c54d2cd1045c6738abc04c4ba Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 18 Jul 2022 15:22:06 +0100 Subject: [PATCH 25/42] FTUE tweaks - Allow login using a phone number. - Update the server when entering a full MXID during registration. - Reset the authentication service back to matrix.org after onboarding completes. - Disable zoom on ReCaptcha to fix responsiveness issue. - Tidy up unused methods. --- Riot/Assets/en.lproj/Vector.strings | 4 +- Riot/Generated/Strings.swift | 4 +- .../Onboarding/OnboardingCoordinator.swift | 3 + .../MatrixSDK/AuthenticationService.swift | 26 +-------- .../Service/MatrixSDK/LoginWizard.swift | 19 +++---- .../AuthenticationLoginCoordinator.swift | 4 +- .../AuthenticationLoginViewModelTests.swift | 3 + .../View/AuthenticationReCaptchaWebView.swift | 2 +- .../AuthenticationRegistrationModels.swift | 7 +++ .../AuthenticationRegistrationViewModel.swift | 10 ++++ ...icationRegistrationViewModelProtocol.swift | 8 +++ ...uthenticationRegistrationCoordinator.swift | 56 +++++++++++++++++-- ...enticationRegistrationViewModelTests.swift | 39 +++++++++++++ .../AuthenticationRegistrationScreen.swift | 4 +- .../AuthenticationServiceTests.swift | 24 ++++++-- changelog.d/6428.wip | 1 + changelog.d/6429.wip | 1 + changelog.d/6430.wip | 1 + 18 files changed, 162 insertions(+), 54 deletions(-) create mode 100644 changelog.d/6428.wip create mode 100644 changelog.d/6429.wip create mode 100644 changelog.d/6430.wip diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index d30598320..e3766c7b0 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -121,7 +121,7 @@ "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"; @@ -139,7 +139,7 @@ "onboarding_avatar_accessibility_label" = "Profile picture"; "onboarding_celebration_title" = "Looking good!"; -"onboarding_celebration_message" = "Head to settings anytime to update your profile."; +"onboarding_celebration_message" = "Head to settings anytime to update your profile"; "onboarding_celebration_button" = "Let's go"; // MARK: Authentication diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 86b87d7dc..f3bed0f26 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4127,7 +4127,7 @@ public class VectorL10n: NSObject { public static var onboardingCelebrationButton: String { return VectorL10n.tr("Vector", "onboarding_celebration_button") } - /// Head to settings anytime to update your profile. + /// Head to settings anytime to update your profile public static var onboardingCelebrationMessage: String { return VectorL10n.tr("Vector", "onboarding_celebration_message") } @@ -4139,7 +4139,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) } diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index ce40cd3c5..3d89c3579 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -566,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. diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift index 695da631b..0ef46245d 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -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. diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift index 3871e720f..f56319f37 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift @@ -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. diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index 97d3a42ca..080917097 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -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) diff --git a/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift index 013eb0ec7..dde66c2fb 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift @@ -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() { diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift index f4a895738..fbd000cb5 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift @@ -92,7 +92,7 @@ struct AuthenticationRecaptchaWebView: UIViewRepresentable { """ - +