mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-26 03:16:02 +03:00
Merge pull request #6108 from vector-im/feature/adm/ftue-msisdn-entry
FTUE - Msisdn (phone number) entry
This commit is contained in:
commit
e326188aa8
18 changed files with 446 additions and 21 deletions
|
@ -110,6 +110,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFrag
|
|||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyWaitForEmailFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthPersonalizationCompleteFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthPhoneEntryFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
|
||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
|
||||
|
@ -509,6 +510,11 @@ interface FragmentModule {
|
|||
@FragmentKey(FtueAuthEmailEntryFragment::class)
|
||||
fun bindFtueAuthEmailEntryFragment(fragment: FtueAuthEmailEntryFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthPhoneEntryFragment::class)
|
||||
fun bindFtueAuthPhoneEntryFragment(fragment: FtueAuthPhoneEntryFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(FtueAuthChooseDisplayNameFragment::class)
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
|||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -193,6 +194,9 @@ object VectorStaticModule {
|
|||
return analyticsConfig
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesBuildMeta() = BuildMeta()
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package im.vector.app.core.extensions
|
||||
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
@ -79,3 +81,12 @@ fun TextInputLayout.setOnFocusLostListener(action: () -> Unit) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TextInputLayout.autofillPhoneNumber() = setAutofillHint(HintConstants.AUTOFILL_HINT_PHONE_NUMBER)
|
||||
fun TextInputLayout.autofillEmail() = setAutofillHint(HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS)
|
||||
|
||||
private fun TextInputLayout.setAutofillHint(hintType: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setAutofillHints(hintType)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,6 +305,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
RegistrationActionHandler.Result.StartRegistration -> _viewEvents.post(OnboardingViewEvents.DisplayStartRegistration)
|
||||
RegistrationActionHandler.Result.UnsupportedStage -> _viewEvents.post(OnboardingViewEvents.DisplayRegistrationFallback)
|
||||
is RegistrationActionHandler.Result.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email))
|
||||
is RegistrationActionHandler.Result.SendMsisdnSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendMsisdnSuccess(it.msisdn.msisdn))
|
||||
is RegistrationActionHandler.Result.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause))
|
||||
RegistrationActionHandler.Result.MissingNextStage -> {
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(IllegalStateException("No next registration stage found")))
|
||||
|
|
|
@ -26,6 +26,7 @@ import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesCom
|
|||
import kotlinx.coroutines.flow.first
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
@ -47,7 +48,8 @@ class RegistrationActionHandler @Inject constructor(
|
|||
else -> when (result) {
|
||||
is RegistrationResult.Complete -> Result.RegistrationComplete(result.session)
|
||||
is RegistrationResult.NextStep -> processFlowResult(result, state)
|
||||
is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email)
|
||||
is RegistrationResult.SendEmailSuccess -> Result.SendEmailSuccess(result.email.email)
|
||||
is RegistrationResult.SendMsisdnSuccess -> Result.SendMsisdnSuccess(result.msisdn)
|
||||
is RegistrationResult.Error -> Result.Error(result.cause)
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +97,7 @@ class RegistrationActionHandler @Inject constructor(
|
|||
data class NextStage(val stage: Stage) : Result
|
||||
data class Error(val cause: Throwable) : Result
|
||||
data class SendEmailSuccess(val email: String) : Result
|
||||
data class SendMsisdnSuccess(val msisdn: RegisterThreePid.Msisdn) : Result
|
||||
object MissingNextStage : Result
|
||||
object StartRegistration : Result
|
||||
object UnsupportedStage : Result
|
||||
|
|
|
@ -59,7 +59,8 @@ class RegistrationWizardActionDelegate @Inject constructor(
|
|||
onSuccess = { it.toRegistrationResult() },
|
||||
onFailure = {
|
||||
when {
|
||||
action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email)
|
||||
action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid)
|
||||
action.threePid is RegisterThreePid.Msisdn && it.is401() -> RegistrationResult.SendMsisdnSuccess(action.threePid)
|
||||
else -> RegistrationResult.Error(it)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +96,8 @@ sealed interface RegistrationResult {
|
|||
data class Error(val cause: Throwable) : RegistrationResult
|
||||
data class Complete(val session: Session) : RegistrationResult
|
||||
data class NextStep(val flowResult: FlowResult) : RegistrationResult
|
||||
data class SendEmailSuccess(val email: String) : RegistrationResult
|
||||
data class SendEmailSuccess(val email: RegisterThreePid.Email) : RegistrationResult
|
||||
data class SendMsisdnSuccess(val msisdn: RegisterThreePid.Msisdn) : RegistrationResult
|
||||
}
|
||||
|
||||
sealed interface RegisterAction {
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.core.extensions.associateContentStateWith
|
||||
import im.vector.app.core.extensions.autofillEmail
|
||||
import im.vector.app.core.extensions.clearErrorOnChange
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
|
@ -47,6 +48,7 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
|
|||
views.emailEntryInput.setOnImeDoneListener { updateEmail() }
|
||||
views.emailEntryInput.clearErrorOnChange(viewLifecycleOwner)
|
||||
views.emailEntrySubmit.debouncedClicks { updateEmail() }
|
||||
views.emailEntryInput.autofillEmail()
|
||||
}
|
||||
|
||||
private fun updateEmail() {
|
||||
|
|
|
@ -36,7 +36,6 @@ import im.vector.app.core.extensions.setTextOrHide
|
|||
import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding
|
||||
import im.vector.app.features.login.TextInputFormFragmentMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -226,12 +225,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
|
|||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
TextInputFormFragmentMode.SetMsisdn -> {
|
||||
if (throwable.is401()) {
|
||||
// This is normal use case, we go to the enter code screen
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendMsisdnSuccess(viewModel.currentThreePid ?: "")))
|
||||
} else {
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
TextInputFormFragmentMode.ConfirmMsisdn -> {
|
||||
when {
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.associateContentStateWith
|
||||
import im.vector.app.core.extensions.autofillPhoneNumber
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.editText
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.databinding.FragmentFtuePhoneInputBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthPhoneEntryFragment @Inject constructor(
|
||||
private val phoneNumberParser: PhoneNumberParser
|
||||
) : AbstractFtueAuthFragment<FragmentFtuePhoneInputBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtuePhoneInputBinding {
|
||||
return FragmentFtuePhoneInputBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
views.phoneEntryInput.associateContentStateWith(button = views.phoneEntrySubmit)
|
||||
views.phoneEntryInput.setOnImeDoneListener { updatePhoneNumber() }
|
||||
views.phoneEntrySubmit.debouncedClicks { updatePhoneNumber() }
|
||||
|
||||
views.phoneEntryInput.editText().textChanges()
|
||||
.onEach {
|
||||
views.phoneEntryInput.error = null
|
||||
views.phoneEntrySubmit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
|
||||
views.phoneEntryInput.autofillPhoneNumber()
|
||||
}
|
||||
|
||||
private fun updatePhoneNumber() {
|
||||
val number = views.phoneEntryInput.content()
|
||||
|
||||
when (val result = phoneNumberParser.parseInternationalNumber(number)) {
|
||||
PhoneNumberParser.Result.ErrorInvalidNumber -> views.phoneEntryInput.error = getString(R.string.login_msisdn_error_other)
|
||||
PhoneNumberParser.Result.ErrorMissingInternationalCode -> views.phoneEntryInput.error = getString(R.string.login_msisdn_error_not_international)
|
||||
is PhoneNumberParser.Result.Success -> {
|
||||
val (countryCode, phoneNumber) = result
|
||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(phoneNumber, countryCode))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(throwable: Throwable) {
|
||||
views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
|
||||
}
|
||||
}
|
|
@ -388,12 +388,21 @@ class FtueAuthVariant(
|
|||
when (stage) {
|
||||
is Stage.ReCaptcha -> onCaptcha(stage)
|
||||
is Stage.Email -> onEmail(stage)
|
||||
is Stage.Msisdn -> addRegistrationStageFragmentToBackstack(
|
||||
is Stage.Msisdn -> onMsisdn(stage)
|
||||
is Stage.Terms -> onTerms(stage)
|
||||
else -> Unit // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMsisdn(stage: Stage) {
|
||||
when {
|
||||
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(
|
||||
FtueAuthPhoneEntryFragment::class.java
|
||||
)
|
||||
else -> addRegistrationStageFragmentToBackstack(
|
||||
FtueAuthGenericTextInputFormFragment::class.java,
|
||||
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
|
||||
)
|
||||
is Stage.Terms -> onTerms(stage)
|
||||
else -> Unit // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import javax.inject.Inject
|
||||
|
||||
class PhoneNumberParser @Inject constructor(
|
||||
private val phoneNumberUtil: PhoneNumberUtil
|
||||
) {
|
||||
|
||||
fun parseInternationalNumber(rawPhoneNumber: String): Result {
|
||||
return when {
|
||||
rawPhoneNumber.doesNotStartWith("+") -> Result.ErrorMissingInternationalCode
|
||||
else -> parseNumber(rawPhoneNumber)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseNumber(rawPhoneNumber: String) = try {
|
||||
val phoneNumber = phoneNumberUtil.parse(rawPhoneNumber, null)
|
||||
Result.Success(phoneNumberUtil.getRegionCodeForCountryCode(phoneNumber.countryCode), rawPhoneNumber)
|
||||
} catch (e: NumberParseException) {
|
||||
Result.ErrorInvalidNumber
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
object ErrorMissingInternationalCode : Result
|
||||
object ErrorInvalidNumber : Result
|
||||
data class Success(val countryCode: String, val phoneNumber: String) : Result
|
||||
}
|
||||
|
||||
private fun String.doesNotStartWith(input: String) = !startsWith(input)
|
||||
}
|
11
vector/src/main/res/drawable/ic_ftue_phone.xml
Normal file
11
vector/src/main/res/drawable/ic_ftue_phone.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="71dp"
|
||||
android:height="70dp"
|
||||
android:viewportWidth="71"
|
||||
android:viewportHeight="70">
|
||||
<path
|
||||
android:fillColor="#ff0000"
|
||||
android:pathData="M29.378,41.602C31.28,43.655 35.858,47.21 37.106,47.94C37.18,47.983 37.264,48.033 37.359,48.09C39.263,49.221 45.229,52.768 49.576,49.449C52.944,46.877 51.848,43.985 50.715,43.125C49.939,42.521 47.652,40.857 45.501,39.359C43.389,37.887 42.211,39.066 41.415,39.863C41.401,39.878 41.386,39.892 41.372,39.907L39.77,41.508C39.362,41.916 38.742,41.767 38.148,41.301C36.015,39.677 34.447,38.11 33.662,37.325L33.655,37.318C32.871,36.534 31.323,34.984 29.699,32.852C29.233,32.258 29.084,31.638 29.492,31.23L31.093,29.628C31.108,29.614 31.122,29.599 31.137,29.584C31.934,28.788 33.113,27.611 31.641,25.499C30.143,23.347 28.479,21.061 27.875,20.285C27.015,19.151 24.122,18.056 21.551,21.424C18.232,25.771 21.778,31.737 22.91,33.641C22.966,33.736 23.017,33.82 23.06,33.894C23.789,35.142 27.325,39.7 29.378,41.602Z"
|
||||
tools:ignore="VectorPath" />
|
||||
</vector>
|
131
vector/src/main/res/layout/fragment_ftue_phone_input.xml
Normal file
131
vector/src/main/res/layout/fragment_ftue_phone_input.xml
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/LoginFormScrollView"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:colorBackground"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/phoneEntryGutterStart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/phoneEntryGutterEnd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/headerSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/phoneEntryHeaderIcon"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/phoneEntryHeaderIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?colorSecondary"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_ftue_phone"
|
||||
app:layout_constraintBottom_toTopOf="@id/phoneEntryHeaderTitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||
app:layout_constraintHeight_percent="0.12"
|
||||
app:layout_constraintStart_toStartOf="@id/phoneEntryGutterStart"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/palette_white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/phoneEntryHeaderTitle"
|
||||
style="@style/Widget.Vector.TextView.Title.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_phone_title"
|
||||
android:textColor="?vctr_content_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/phoneEntryHeaderSubtitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/phoneEntryGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/phoneEntryHeaderIcon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/phoneEntryHeaderSubtitle"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_phone_subtitle"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/phoneEntryGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/phoneEntryHeaderTitle" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/titleContentSpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/phoneEntryInput"
|
||||
app:layout_constraintHeight_percent="0.03"
|
||||
app:layout_constraintTop_toBottomOf="@id/phoneEntryHeaderSubtitle" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/phoneEntryInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/ftue_auth_phone_entry_title"
|
||||
app:endIconMode="clear_text"
|
||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/phoneEntryGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="phone"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Space
|
||||
android:id="@+id/entrySpacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/phoneEntrySubmit"
|
||||
app:layout_constraintHeight_percent="0.03"
|
||||
app:layout_constraintTop_toBottomOf="@id/phoneEntryInput"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/phoneEntrySubmit"
|
||||
style="@style/Widget.Vector.Button.Login"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_set_email_submit"
|
||||
android:textAllCaps="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/phoneEntryGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -36,6 +36,10 @@
|
|||
<string name="ftue_auth_email_title">Enter your email address</string>
|
||||
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
|
||||
<string name="ftue_auth_email_entry_title">Email Address</string>
|
||||
<string name="ftue_auth_phone_title">Enter your phone number</string>
|
||||
<string name="ftue_auth_phone_subtitle">This will help verify your account and enables password recovery.</string>
|
||||
<string name="ftue_auth_phone_entry_title">Phone Number</string>
|
||||
|
||||
<string name="ftue_auth_reset_password_email_subtitle">We will send you a verification link.</string>
|
||||
<string name="ftue_auth_reset_password_breaker_title">Check your email.</string>
|
||||
<string name="ftue_auth_new_password_entry_title">New Password</string>
|
||||
|
|
|
@ -38,7 +38,8 @@ private const val AN_INITIAL_DEVICE_NAME = "a device name"
|
|||
private const val A_CAPTCHA_RESPONSE = "a captcha response"
|
||||
private const val A_PID_CODE = "a pid code"
|
||||
private const val EMAIL_VALIDATED_DELAY = 10000L
|
||||
private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email")
|
||||
private val AN_EMAIL_PID_TO_REGISTER = RegisterThreePid.Email("an email")
|
||||
private val A_PHONE_PID_TO_REGISTER = RegisterThreePid.Msisdn("+11111111111", countryCode = "GB")
|
||||
|
||||
class RegistrationWizardActionDelegateTest {
|
||||
|
||||
|
@ -55,7 +56,7 @@ class RegistrationWizardActionDelegateTest {
|
|||
case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) },
|
||||
case(RegisterAction.AcceptTerms) { acceptTerms() },
|
||||
case(RegisterAction.RegisterDummy) { dummy() },
|
||||
case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) },
|
||||
case(RegisterAction.AddThreePid(AN_EMAIL_PID_TO_REGISTER)) { addThreePid(AN_EMAIL_PID_TO_REGISTER) },
|
||||
case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() },
|
||||
case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) },
|
||||
case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) },
|
||||
|
@ -69,14 +70,14 @@ class RegistrationWizardActionDelegateTest {
|
|||
|
||||
@Test
|
||||
fun `given adding an email ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest {
|
||||
fakeRegistrationWizard.givenAddEmailThreePidErrors(
|
||||
fakeRegistrationWizard.givenAddThreePidErrors(
|
||||
cause = a401ServerError(),
|
||||
email = A_PID_TO_REGISTER.email
|
||||
threePid = AN_EMAIL_PID_TO_REGISTER
|
||||
)
|
||||
|
||||
val result = registrationActionHandler.executeAction(RegisterAction.AddThreePid(A_PID_TO_REGISTER))
|
||||
val result = registrationActionHandler.executeAction(RegisterAction.AddThreePid(AN_EMAIL_PID_TO_REGISTER))
|
||||
|
||||
result shouldBeEqualTo RegistrationResult.SendEmailSuccess(A_PID_TO_REGISTER.email)
|
||||
result shouldBeEqualTo RegistrationResult.SendEmailSuccess(AN_EMAIL_PID_TO_REGISTER)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -110,6 +111,18 @@ class RegistrationWizardActionDelegateTest {
|
|||
result shouldBeEqualTo RegistrationResult.Complete(A_SESSION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given adding an Msisdn ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest {
|
||||
fakeRegistrationWizard.givenAddThreePidErrors(
|
||||
cause = a401ServerError(),
|
||||
threePid = A_PHONE_PID_TO_REGISTER
|
||||
)
|
||||
|
||||
val result = registrationActionHandler.executeAction(RegisterAction.AddThreePid(A_PHONE_PID_TO_REGISTER))
|
||||
|
||||
result shouldBeEqualTo RegistrationResult.SendMsisdnSuccess(A_PHONE_PID_TO_REGISTER)
|
||||
}
|
||||
|
||||
private suspend fun testSuccessfulActionDelegation(case: Case) {
|
||||
val fakeRegistrationWizard = FakeRegistrationWizard()
|
||||
val authenticationService = FakeAuthenticationService().also { it.givenRegistrationWizard(fakeRegistrationWizard) }
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import im.vector.app.test.fakes.FakePhoneNumberUtil
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
private const val AN_INTERNATIONAL_PHONE_NUMBER = "+4411111111111"
|
||||
private const val A_NON_INTERNATIONAL_PHONE_NUMBER = "111111111111"
|
||||
private const val AN_INVALID_INTERNATIONAL_NUMBER = "+abc"
|
||||
private const val A_COUNTRY_CODE = "GB"
|
||||
private const val A_COUNTRY_CALLING_CODE = 44
|
||||
|
||||
class PhoneNumberParserTest {
|
||||
|
||||
private val fakePhoneNumberUtil = FakePhoneNumberUtil()
|
||||
private val phoneNumberParser = PhoneNumberParser(fakePhoneNumberUtil.instance)
|
||||
|
||||
@Test
|
||||
fun `given a calling code and country code are successfully read when parsing, then returns success with country code`() {
|
||||
fakePhoneNumberUtil.givenCountryCallingCodeFor(AN_INTERNATIONAL_PHONE_NUMBER, callingCode = A_COUNTRY_CALLING_CODE)
|
||||
fakePhoneNumberUtil.givenRegionCodeFor(A_COUNTRY_CALLING_CODE, countryCode = A_COUNTRY_CODE)
|
||||
|
||||
val result = phoneNumberParser.parseInternationalNumber(AN_INTERNATIONAL_PHONE_NUMBER)
|
||||
|
||||
result shouldBeEqualTo PhoneNumberParser.Result.Success(A_COUNTRY_CODE, AN_INTERNATIONAL_PHONE_NUMBER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a non internation phone number, when parsing, then returns MissingInternationCode error`() {
|
||||
val result = phoneNumberParser.parseInternationalNumber(A_NON_INTERNATIONAL_PHONE_NUMBER)
|
||||
|
||||
result shouldBeEqualTo PhoneNumberParser.Result.ErrorMissingInternationalCode
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an invalid phone number, when parsing, then returns ErrorInvalidNumber error`() {
|
||||
fakePhoneNumberUtil.givenFailsToParse(AN_INVALID_INTERNATIONAL_NUMBER)
|
||||
|
||||
val result = phoneNumberParser.parseInternationalNumber(AN_INVALID_INTERNATIONAL_NUMBER)
|
||||
|
||||
result shouldBeEqualTo PhoneNumberParser.Result.ErrorInvalidNumber
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class FakePhoneNumberUtil {
|
||||
|
||||
val instance = mockk<PhoneNumberUtil>()
|
||||
|
||||
fun givenCountryCallingCodeFor(phoneNumber: String, callingCode: Int) {
|
||||
every { instance.parse(phoneNumber, null) } returns Phonenumber.PhoneNumber().setCountryCode(callingCode)
|
||||
}
|
||||
|
||||
fun givenRegionCodeFor(callingCode: Int, countryCode: String) {
|
||||
every { instance.getRegionCodeForCountryCode(callingCode) } returns countryCode
|
||||
}
|
||||
|
||||
fun givenFailsToParse(phoneNumber: String) {
|
||||
every { instance.parse(phoneNumber, null) } throws NumberParseException(NumberParseException.ErrorType.NOT_A_NUMBER, "")
|
||||
}
|
||||
}
|
|
@ -30,8 +30,8 @@ class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
|
|||
coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
|
||||
}
|
||||
|
||||
fun givenAddEmailThreePidErrors(cause: Throwable, email: String) {
|
||||
coEvery { addThreePid(RegisterThreePid.Email(email)) } throws cause
|
||||
fun givenAddThreePidErrors(cause: Throwable, threePid: RegisterThreePid) {
|
||||
coEvery { addThreePid(threePid) } throws cause
|
||||
}
|
||||
|
||||
fun givenCheckIfEmailHasBeenValidatedErrors(errors: List<Throwable>, finally: RegistrationResult? = null) {
|
||||
|
|
Loading…
Reference in a new issue