Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication: Verify MSISDN screen #6205

Merged
merged 12 commits into from
May 26, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "authentication_msisdn_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions Riot/Assets/en.lproj/Untranslated.strings
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
"authentication_verify_email_waiting_hint" = "Did not receive an email?";
"authentication_verify_email_waiting_button" = "Resend email";

"authentication_verify_msisdn_input_title" = "Enter your phone number";
"authentication_verify_msisdn_input_message" = "This will help verify your account and enables password recovery.";
"authentication_verify_msisdn_text_field_placeholder" = "Phone Number";
"authentication_verify_msisdn_otp_text_field_placeholder" = "Verification Code";
"authentication_verify_msisdn_waiting_title" = "Confirm your phone number";
"authentication_verify_msisdn_waiting_message" = "We just sent a code to %@. Enter it below to verify it’s you.";
"authentication_verify_msisdn_waiting_button" = "Resend code";
"authentication_verify_msisdn_invalid_phone_number" = "Invalid phone number";

"authentication_terms_title" = "Privacy policy";
"authentication_terms_message" = "Please read through T&C. You must accept in order to continue.";
"authentication_terms_policy_url_error" = "Unable to find the selected policy. Please try again later.";
Expand Down
1 change: 1 addition & 0 deletions Riot/Generated/Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal class Asset: NSObject {
internal static let socialLoginButtonGoogle = ImageAsset(name: "social_login_button_google")
internal static let socialLoginButtonTwitter = ImageAsset(name: "social_login_button_twitter")
internal static let authenticationEmailIcon = ImageAsset(name: "authentication_email_icon")
internal static let authenticationMsisdnIcon = ImageAsset(name: "authentication_msisdn_icon")
internal static let authenticationServerSelectionIcon = ImageAsset(name: "authentication_server_selection_icon")
internal static let authenticationSsoIconApple = ImageAsset(name: "authentication_sso_icon_apple")
internal static let authenticationSsoIconFacebook = ImageAsset(name: "authentication_sso_icon_facebook")
Expand Down
32 changes: 32 additions & 0 deletions Riot/Generated/UntranslatedStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ 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")
}
/// Enter your phone number
static var authenticationVerifyMsisdnInputTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_input_title")
}
/// Invalid phone number
static var authenticationVerifyMsisdnInvalidPhoneNumber: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_invalid_phone_number")
}
/// Verification Code
static var authenticationVerifyMsisdnOtpTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_otp_text_field_placeholder")
}
/// Phone Number
static var authenticationVerifyMsisdnTextFieldPlaceholder: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_text_field_placeholder")
}
/// Resend code
static var authenticationVerifyMsisdnWaitingButton: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_button")
}
/// We just sent a code to %@. Enter it below to verify it’s you.
static func authenticationVerifyMsisdnWaitingMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_message", p1)
}
/// Confirm your phone number
static var authenticationVerifyMsisdnWaitingTitle: String {
return VectorL10n.tr("Untranslated", "authentication_verify_msisdn_waiting_title")
}
/// Choose from files
static var imagePickerActionFiles: String {
return VectorL10n.tr("Untranslated", "image_picker_action_files")
Expand Down
14 changes: 13 additions & 1 deletion Riot/Modules/Onboarding/AuthenticationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,19 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
/// Shows the verify email screen.
@MainActor private func showVerifyMSISDNScreen(registrationWizard: RegistrationWizard) {
MXLog.debug("[AuthenticationCoordinator] showVerifyMSISDNScreen")
fatalError("Phone verification not implemented yet.")

let parameters = AuthenticationVerifyMsisdnCoordinatorParameters(registrationWizard: registrationWizard)
let coordinator = AuthenticationVerifyMsisdnCoordinator(parameters: parameters)
coordinator.callback = { [weak self] result in
self?.registrationStageDidComplete(with: result)
}

coordinator.start()
add(childCoordinator: coordinator)

navigationRouter.setRootModule(coordinator, hideNavigationBar: false, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}

/// Displays the next view in the registration flow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,16 @@ enum RegistrationError: String, LocalizedError {
case threePIDValidationFailure
case threePIDClientFailure
case waitingForThreePIDValidation
case invalidPhoneNumber

var errorDescription: String? {
switch self {
case .registrationDisabled:
return VectorL10n.loginErrorRegistrationIsNotSupported
case .threePIDValidationFailure, .threePIDClientFailure:
return VectorL10n.authMsisdnValidationError
case .invalidPhoneNumber:
return VectorL10n.authenticationVerifyMsisdnInvalidPhoneNumber
default:
return VectorL10n.errorCommonMessage
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class RegistrationWizard {
sessionID: threePIDData.registrationResponse.sessionID,
code: code)

#warning("Seems odd to pass a nil baseURL and then the url as the path, yet this is how MXK3PID works")
// Seems odd to pass a nil baseURL and then the url as the path, yet this is how MXK3PID works"
guard let httpClient = MXHTTPClient(baseURL: nil, andOnUnrecognizedCertificateBlock: nil) else {
MXLog.error("[RegistrationWizard] validateThreePid: Failed to create an MXHTTPClient.")
throw RegistrationError.threePIDClientFailure
Expand All @@ -213,7 +213,6 @@ class RegistrationWizard {
}

let parameters = threePIDData.registrationParameters
MXLog.failure("This method used to add a 3-second delay to the request. This should be moved to the caller of `handleValidateThreePID`.")
return try await performRegistrationRequest(parameters: parameters)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable {
switch registrationError {
case .registrationDisabled:
authenticationRegistrationViewModel.displayError(.registrationDisabled)
case .createAccountNotCalled, .missingThreePIDData, .missingThreePIDURL, .threePIDClientFailure, .threePIDValidationFailure, .waitingForThreePIDValidation:
case .createAccountNotCalled, .missingThreePIDData, .missingThreePIDURL, .threePIDClientFailure, .threePIDValidationFailure, .waitingForThreePIDValidation, .invalidPhoneNumber:
// Shouldn't happen at this stage
authenticationRegistrationViewModel.displayError(.unknown)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ enum AuthenticationVerifyEmailViewModelResult {
case resend
/// Cancel the flow.
case cancel
/// Go back to the email form
case goBack
}

// MARK: View
Expand Down Expand Up @@ -65,6 +67,8 @@ enum AuthenticationVerifyEmailViewAction {
case resend
/// Cancel the flow.
case cancel
/// Go back to enter email adress screen
case goBack
}

enum AuthenticationVerifyEmailErrorType: Hashable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,18 @@ class AuthenticationVerifyEmailViewModel: AuthenticationVerifyEmailViewModelType
Task { await callback?(.resend) }
case .cancel:
Task { await callback?(.cancel) }
case .goBack:
Task { await callback?(.goBack) }
}
}

@MainActor func updateForSentEmail() {
state.hasSentEmail = true
}

@MainActor func goBackToEnterEmailForm() {
state.hasSentEmail = false
}

@MainActor func displayError(_ type: AuthenticationVerifyEmailErrorType) {
switch type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ protocol AuthenticationVerifyEmailViewModelProtocol {

/// Updates the view to reflect that a verification email was successfully sent.
@MainActor func updateForSentEmail()

/// Goes back to the email form
@MainActor func goBackToEnterEmailForm()

/// Display an error to the user.
@MainActor func displayError(_ type: AuthenticationVerifyEmailErrorType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable {
self.resendEmail()
case .cancel:
self.callback?(.cancel)
case .goBack:
self.authenticationVerifyEmailViewModel.goBackToEnterEmailForm()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {

XCTAssertFalse(app.staticTexts["waitingTitleLabel"].exists, "The waiting title should be hidden until an email is sent.")
XCTAssertFalse(app.staticTexts["waitingMessageLabel"].exists, "The waiting message should be hidden until an email is sent.")

let cancelButton = app.navigationBars.firstMatch.buttons["cancelButton"]
XCTAssertTrue(cancelButton.exists, "Cancel button should be shown.")
XCTAssertEqual(cancelButton.label, "Cancel")
}

func verifyEnteredAddress() {
Expand All @@ -69,6 +73,10 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {

XCTAssertFalse(app.staticTexts["waitingTitleLabel"].exists, "The waiting title should be hidden until an email is sent.")
XCTAssertFalse(app.staticTexts["waitingMessageLabel"].exists, "The waiting message should be hidden until an email is sent.")

let cancelButton = app.navigationBars.firstMatch.buttons["cancelButton"]
XCTAssertTrue(cancelButton.exists, "Cancel button should be shown.")
XCTAssertEqual(cancelButton.label, "Cancel")
}

func verifyWaitingForEmailLink() {
Expand All @@ -79,6 +87,10 @@ class AuthenticationVerifyEmailUITests: MockScreenTest {

XCTAssertTrue(app.staticTexts["waitingTitleLabel"].exists, "The waiting title should be shown once an email has been sent.")
XCTAssertTrue(app.staticTexts["waitingMessageLabel"].exists, "The waiting title should be shown once an email has been sent.")

let backButton = app.navigationBars.firstMatch.buttons["cancelButton"]
XCTAssertTrue(backButton.exists, "Back button should be shown.")
XCTAssertEqual(backButton.label, "Back")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import XCTest
@testable import RiotSwiftUI

class AuthenticationVerifyEmailViewModelTests: XCTestCase {
private enum Constants {
static let counterInitialValue = 0
}


var viewModel: AuthenticationVerifyEmailViewModelProtocol!
var context: AuthenticationVerifyEmailViewModelType.Context!

Expand All @@ -31,15 +28,22 @@ class AuthenticationVerifyEmailViewModelTests: XCTestCase {
context = viewModel.context
}

func testSentEmailState() async {
@MainActor func testSentEmailState() async {
// Given a view model where the user hasn't yet sent the verification email.
XCTAssertFalse(context.viewState.hasSentEmail, "The view model should start with hasSentEmail equal to false.")

// When updating to indicate that an email has been send.
let task = Task { await viewModel.updateForSentEmail() }
_ = await task.result
viewModel.updateForSentEmail()

// Then the view model should update to reflect a sent email.
XCTAssertTrue(context.viewState.hasSentEmail, "The view model should update hasSentEmail after sending an email.")
}

@MainActor func testGoBack() async {
viewModel.updateForSentEmail()

viewModel.goBackToEnterEmailForm()

XCTAssertFalse(context.viewState.hasSentEmail, "The view model should update hasSentEmail after going back.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,14 @@ struct AuthenticationVerifyEmailScreen: View {
/// A simple toolbar with a cancel button.
var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
Button(VectorL10n.cancel) {
viewModel.send(viewAction: .cancel)
Button(viewModel.viewState.hasSentEmail ? VectorL10n.back : VectorL10n.cancel) {
if viewModel.viewState.hasSentEmail {
viewModel.send(viewAction: .goBack)
} else {
viewModel.send(viewAction: .cancel)
}
}
.accessibilityIdentifier("cancelButton")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Copyright 2021 New Vector Ltd
//
// 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
//
// http://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.
//

import SwiftUI

// MARK: View model

enum AuthenticationVerifyMsisdnViewModelResult {
/// Send an SMS to the associated phone number and country code.
case send(String)
/// Submit the OTP
case submitOTP(String)
/// Send the email once more.
case resend
/// Cancel the flow.
case cancel
/// Go back to the msisdn form
case goBack
}

// MARK: View

struct AuthenticationVerifyMsisdnViewState: BindableState {
/// An SMS has been sent.
var hasSentSMS = false
/// View state that can be bound to from SwiftUI.
var bindings: AuthenticationVerifyMsisdnBindings

/// Whether the phone number is valid and the user can continue.
var hasInvalidPhoneNumber: Bool {
bindings.phoneNumber.isEmpty
}

/// Whether the OTP is valid and the user can continue.
var hasInvalidOTP: Bool {
bindings.otp.isEmpty
}
}

struct AuthenticationVerifyMsisdnBindings {
/// The phone number input by the user.
var phoneNumber: String
/// The OTP
var otp: String
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<AuthenticationVerifyMsisdnErrorType>?
}

enum AuthenticationVerifyMsisdnViewAction {
/// Send an SMS to the entered phone number.
case send
/// Submit OTP to verify phone number
case submitOTP
/// Send the SMS once more.
case resend
/// Cancel the flow.
case cancel
/// Go back to msisdn form
case goBack
}

enum AuthenticationVerifyMsisdnErrorType: Hashable {
/// An error response from the homeserver.
case mxError(String)
/// User entered an invalid phone number
case invalidPhoneNumber
/// An unknown error occurred.
ismailgulek marked this conversation as resolved.
Show resolved Hide resolved
case unknown
}
Loading