diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index e76f0ad672..143818f3e5 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index bc0bccfa1b..cbd34fa05b 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt index 40019c5d64..d3ee765780 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index b5f5682be1..56f95db2be 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -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"))) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt index 76b1492cc3..488a96beb9 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt index e27aa9b2ab..8635c1e203 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationWizardActionDelegate.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt index 523d576120..1d85c75fa1 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index 5e4c954300..edfbcd89b6 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt new file mode 100644 index 0000000000..905af75639 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt @@ -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() { + + 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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 2880b16156..df5c6e79ef 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -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 } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt new file mode 100644 index 0000000000..6a46a466eb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/PhoneNumberParser.kt @@ -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) +} diff --git a/vector/src/main/res/drawable/ic_ftue_phone.xml b/vector/src/main/res/drawable/ic_ftue_phone.xml new file mode 100644 index 0000000000..53884d6d96 --- /dev/null +++ b/vector/src/main/res/drawable/ic_ftue_phone.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_phone_input.xml b/vector/src/main/res/layout/fragment_ftue_phone_input.xml new file mode 100644 index 0000000000..32b54bcef3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_phone_input.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +