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

FTUE - Msisdn (phone number) entry #6108

Merged
merged 7 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -193,6 +194,9 @@ object VectorStaticModule {
return analyticsConfig
}

@Provides
fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance()

@Provides
@Singleton
fun providesBuildMeta() = BuildMeta()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

International phone numbers can also start with "00" instead of "+". Perhaps we should check that too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL

To avoid confusion especially in international context, a plus sign (+) is often used as a graphic symbol of the international access code; it informs the caller to replace it with the prefix code appropriate for their country.[3] Additionally, the GSM mobile telephony standard allows the use of the plus sign in place of the international call prefix; the mobile operator then automatically converts the plus sign to the correct international prefix, depending on the location where the phone is being used. This enables callers to use the same stored number when calling from any country.

https://en.wikipedia.org/wiki/International_call_prefix

2022-06-29T16:05:41,376511680+01:00

tl;dr mobile networks automatically covert from + to the correct IDD prefix

we would have to support multiple variants of 00, 011 etc which wouldn't scale too well and for the most part should be safe to replace with a +

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah...... let's stick with the + 😂

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

}
11 changes: 11 additions & 0 deletions vector/src/main/res/drawable/ic_ftue_phone.xml
Original file line number Diff line number Diff line change
@@ -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>
Loading