diff --git a/changelog.d/5278.wip b/changelog.d/5278.wip new file mode 100644 index 0000000000..c6014dc9ac --- /dev/null +++ b/changelog.d/5278.wip @@ -0,0 +1 @@ +Adds email input and verification screens to the new FTUE onboarding flow diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml b/library/ui-styles/src/main/res/drawable/bg_color_background.xml similarity index 100% rename from library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml rename to library/ui-styles/src/main/res/drawable/bg_color_background.xml diff --git a/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml b/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml new file mode 100644 index 0000000000..cdd4c20a4d --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_waiting_for_email_verification.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + 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 c68a35f4e5..3dba8b797b 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 @@ -101,8 +101,10 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment +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.FtueAuthResetPasswordFragment @@ -474,6 +476,11 @@ interface FragmentModule { @FragmentKey(FtueAuthWaitForEmailFragment::class) fun bindFtueAuthWaitForEmailFragment(fragment: FtueAuthWaitForEmailFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthLegacyWaitForEmailFragment::class) + fun bindFtueAuthLegacyWaitForEmailFragment(fragment: FtueAuthLegacyWaitForEmailFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthWebFragment::class) @@ -494,6 +501,11 @@ interface FragmentModule { @FragmentKey(FtueAuthAccountCreatedFragment::class) fun bindFtueAuthAccountCreatedFragment(fragment: FtueAuthAccountCreatedFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthEmailEntryFragment::class) + fun bindFtueAuthEmailEntryFragment(fragment: FtueAuthEmailEntryFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthChooseDisplayNameFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/extensions/Job.kt b/vector/src/main/java/im/vector/app/core/extensions/Job.kt new file mode 100644 index 0000000000..d9a4332ef2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Job.kt @@ -0,0 +1,33 @@ +/* + * 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.core.extensions + +import kotlinx.coroutines.Job +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Property delegate for automatically cancelling the current job when setting a new value. + */ +fun cancelCurrentOnSet(): ReadWriteProperty = object : ReadWriteProperty { + private var currentJob: Job? = null + override fun getValue(thisRef: Any?, property: KProperty<*>): Job? = currentJob + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Job?) { + currentJob?.cancel() + currentJob = value + } +} 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 d9b92e78b7..205a0f40c4 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,7 +16,11 @@ package im.vector.app.core.extensions +import android.text.Editable +import android.view.View +import android.view.inputmethod.EditorInfo import com.google.android.material.textfield.TextInputLayout +import im.vector.app.core.platform.SimpleTextWatcher import kotlinx.coroutines.flow.map import reactivecircus.flowbinding.android.widget.textChanges @@ -30,3 +34,26 @@ fun TextInputLayout.hasSurroundingSpaces() = editText().text.toString().let { it fun TextInputLayout.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() } fun TextInputLayout.content() = editText().text.toString() + +fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() + +fun TextInputLayout.associateContentStateWith(button: View) { + editText().addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + val newContent = s.toString() + button.isEnabled = newContent.isNotEmpty() + } + }) +} + +fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) { + editText().setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + action() + true + } + else -> false + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt index 69702fc793..aa1917e326 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SpannableUtils.kt @@ -22,6 +22,7 @@ import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import androidx.annotation.ColorInt import me.gujun.android.span.Span +import me.gujun.android.span.span fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { if (match.isEmpty()) return this @@ -56,3 +57,17 @@ fun Span.bullet(text: CharSequence = "", build() }) } + +fun String.colorTerminatingFullStop(@ColorInt color: Int): CharSequence { + val fullStop = "." + return if (endsWith(fullStop)) { + span { + +this@colorTerminatingFullStop.removeSuffix(fullStop) + span(fullStop) { + textColor = color + } + } + } else { + this + } +} 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 03526b47a5..a7caa9d0c2 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 @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.cancelCurrentOnSet import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel @@ -50,7 +51,6 @@ import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.FlowResult -import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.Session @@ -125,12 +125,8 @@ class OnboardingViewModel @AssistedInject constructor( private var loginConfig: LoginConfig? = null - private var currentJob: Job? = null - set(value) { - // Cancel any previous Job - field?.cancel() - field = value - } + private var emailVerificationPollingJob: Job? by cancelCurrentOnSet() + private var currentJob: Job? by cancelCurrentOnSet() override fun handle(action: OnboardingAction) { when (action) { @@ -257,13 +253,19 @@ class OnboardingViewModel @AssistedInject constructor( } private fun handleRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { - currentJob = viewModelScope.launch { + val job = viewModelScope.launch { if (action.hasLoadingState()) { setState { copy(isLoading = true) } } internalRegisterAction(action, onNextRegistrationStepAction) setState { copy(isLoading = false) } } + + // Allow email verification polling to coexist with other jobs + when (action) { + is RegisterAction.CheckIfEmailHasBeenValidated -> emailVerificationPollingJob = job + else -> currentJob = job + } } private suspend fun internalRegisterAction(action: RegisterAction, onNextRegistrationStepAction: (FlowResult) -> Unit) { @@ -275,8 +277,10 @@ class OnboardingViewModel @AssistedInject constructor( // do nothing } else -> when (it) { - is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true) - is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) + is RegistrationResult.Complete -> onSessionCreated(it.session, isAccountCreated = true) + is RegistrationResult.NextStep -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) + is RegistrationResult.SendEmailSuccess -> _viewEvents.post(OnboardingViewEvents.OnSendEmailSuccess(it.email)) + is RegistrationResult.Error -> _viewEvents.post(OnboardingViewEvents.Failure(it.cause)) } } }, @@ -307,6 +311,7 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleResetAction(action: OnboardingAction.ResetAction) { // Cancel any request currentJob = null + emailVerificationPollingJob = null when (action) { OnboardingAction.ResetHomeServerType -> { @@ -790,7 +795,7 @@ class OnboardingViewModel @AssistedInject constructor( } private fun cancelWaitForEmailValidation() { - currentJob = null + emailVerificationPollingJob = null } } 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 b4998d2ba0..7bffe50754 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 @@ -16,26 +16,80 @@ package im.vector.app.features.onboarding +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.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.FlowResponse +import org.matrix.android.sdk.api.auth.registration.RegistrationResult.Success import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.failure.is401 +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject +import org.matrix.android.sdk.api.auth.registration.RegistrationResult as MatrixRegistrationResult class RegistrationActionHandler @Inject constructor() { suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { return when (action) { - RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow() - is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse) - is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms() - is RegisterAction.RegisterDummy -> registrationWizard.dummy() - is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid) - is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid() - is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code) - is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) - is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName) + RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() } + is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) } + is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() } + is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() } + is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action) + is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() } + is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) } + is RegisterAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailIsValidated(registrationWizard, action.delayMillis) + is RegisterAction.CreateAccount -> resultOf { + registrationWizard.createAccount( + action.username, + action.password, + action.initialDeviceName + ) + } } } + + private suspend fun handleAddThreePid(wizard: RegistrationWizard, action: RegisterAction.AddThreePid): RegistrationResult { + return runCatching { wizard.addThreePid(action.threePid) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email) + else -> RegistrationResult.Error(it) + } + } + ) + } + + private tailrec suspend fun handleCheckIfEmailIsValidated(registrationWizard: RegistrationWizard, delayMillis: Long): RegistrationResult { + return runCatching { registrationWizard.checkIfEmailHasBeenValidated(delayMillis) }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { + when { + it.is401() -> null // recursively continue to check with a delay + else -> RegistrationResult.Error(it) + } + } + ) ?: handleCheckIfEmailIsValidated(registrationWizard, 10_000) + } +} + +private inline fun resultOf(block: () -> MatrixRegistrationResult): RegistrationResult { + return runCatching { block() }.fold( + onSuccess = { it.toRegistrationResult() }, + onFailure = { RegistrationResult.Error(it) } + ) +} + +private fun MatrixRegistrationResult.toRegistrationResult() = when (this) { + is FlowResponse -> RegistrationResult.NextStep(flowResult) + is Success -> RegistrationResult.Complete(session) +} + +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 } sealed interface RegisterAction { @@ -56,7 +110,6 @@ sealed interface RegisterAction { } fun RegisterAction.ignoresResult() = when (this) { - is RegisterAction.AddThreePid -> true is RegisterAction.SendAgainThreePid -> true else -> false } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt index 1ce0c544e5..f4cf1e9bea 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseDisplayNameFragment.kt @@ -22,7 +22,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import com.google.android.material.textfield.TextInputLayout +import im.vector.app.core.extensions.hasContent import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.databinding.FragmentFtueDisplayNameBinding import im.vector.app.features.onboarding.OnboardingAction @@ -69,7 +69,7 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth override fun updateWithState(state: OnboardingViewState) { views.displayNameInput.editText?.setText(state.personalizationState.displayName) - views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty() + views.displayNameSubmit.isEnabled = views.displayNameInput.hasContent() } override fun resetViewModel() { @@ -81,5 +81,3 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth return true } } - -private fun TextInputLayout.hasContentEmpty() = !editText?.text.isNullOrEmpty() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt index 2e6057288a..b7a5dc7298 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt @@ -68,7 +68,7 @@ class FtueAuthCombinedServerSelectionFragment @Inject constructor() : AbstractFt views.chooseServerSubmit.debouncedClicks { updateServerUrl() } views.chooseServerInput.editText().textChanges() .onEach { views.chooseServerInput.error = null } - .launchIn(lifecycleScope) + .launchIn(viewLifecycleOwner.lifecycleScope) } private fun updateServerUrl() { 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 new file mode 100644 index 0000000000..ea376709f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -0,0 +1,74 @@ +/* + * 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.core.extensions.associateContentStateWith +import im.vector.app.core.extensions.content +import im.vector.app.core.extensions.editText +import im.vector.app.core.extensions.isEmail +import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.databinding.FragmentFtueEmailInputBinding +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 FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueEmailInputBinding { + return FragmentFtueEmailInputBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.emailEntryInput.associateContentStateWith(button = views.emailEntrySubmit) + views.emailEntryInput.setOnImeDoneListener { updateEmail() } + views.emailEntrySubmit.debouncedClicks { updateEmail() } + + views.emailEntryInput.editText().textChanges() + .onEach { + views.emailEntryInput.error = null + views.emailEntrySubmit.isEnabled = it.isEmail() + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun updateEmail() { + val email = views.emailEntryInput.content() + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(email)))) + } + + override fun onError(throwable: Throwable) { + views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} 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 ce3dee7a19..fce1308d3c 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 @@ -223,12 +223,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA override fun onError(throwable: Throwable) { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - if (throwable.is401()) { - // This is normal use case, we go to the mail waiting screen - viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(viewModel.currentThreePid ?: ""))) - } else { - views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) - } + views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } TextInputFormFragmentMode.SetMsisdn -> { if (throwable.is401()) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt new file mode 100644 index 0000000000..c815f354f0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthLegacyWaitForEmailFragment.kt @@ -0,0 +1,63 @@ +/* + * 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 com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.databinding.FragmentLoginWaitForEmailBinding +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction +import javax.inject.Inject + +/** + * In this screen, the user is asked to check their emails. + */ +class FtueAuthLegacyWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragment() { + + private val params: FtueAuthWaitForEmailFragmentArgument by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmailBinding { + return FragmentLoginWaitForEmailBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupUi() + } + + override fun onResume() { + super.onResume() + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) + } + + override fun onPause() { + super.onPause() + viewModel.handle(OnboardingAction.StopEmailValidationCheck) + } + + private fun setupUi() { + views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email) + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 49e8875cb5..30416bde9e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -22,6 +22,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 import com.airbnb.mvrx.withState @@ -90,7 +92,7 @@ class FtueAuthSplashCarouselFragment @Inject constructor( private fun ViewPager2.registerAutomaticUntilInteractionTransitions() { var scheduledTransition: Job? = null - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + val pageChangingCallback = object : ViewPager2.OnPageChangeCallback() { private var hasUserManuallyInteractedWithCarousel: Boolean = false override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { @@ -104,12 +106,21 @@ class FtueAuthSplashCarouselFragment @Inject constructor( scheduledTransition = scheduleCarouselTransition() } } + } + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + registerOnPageChangeCallback(pageChangingCallback) + } + + override fun onDestroy(owner: LifecycleOwner) { + unregisterOnPageChangeCallback(pageChangingCallback) + } }) } private fun ViewPager2.scheduleCarouselTransition(): Job { val itemCount = adapter?.itemCount ?: throw IllegalStateException("An adapter must be set") - return lifecycleScope.launch { + return viewLifecycleOwner.lifecycleScope.launch { delay(CAROUSEL_ROTATION_DELAY_MS) setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } 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 6f1b85df4f..036f8eb9a2 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 @@ -192,12 +192,7 @@ class FtueAuthVariant( supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } is OnboardingViewEvents.OnSendEmailSuccess -> { - // Pop the enter email Fragment - supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) - addRegistrationStageFragmentToBackstack( - FtueAuthWaitForEmailFragment::class.java, - FtueAuthWaitForEmailFragmentArgument(viewEvents.email), - ) + openWaitForEmailVerification(viewEvents.email) } is OnboardingViewEvents.OnSendMsisdnSuccess -> { // Pop the enter Msisdn Fragment @@ -393,10 +388,7 @@ class FtueAuthVariant( when (stage) { is Stage.ReCaptcha -> onCaptcha(stage) - is Stage.Email -> addRegistrationStageFragmentToBackstack( - FtueAuthGenericTextInputFormFragment::class.java, - FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), - ) + is Stage.Email -> onEmail(stage) is Stage.Msisdn -> addRegistrationStageFragmentToBackstack( FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), @@ -406,6 +398,32 @@ class FtueAuthVariant( } } + private fun onEmail(stage: Stage) { + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthEmailEntryFragment::class.java + ) + else -> addRegistrationStageFragmentToBackstack( + FtueAuthGenericTextInputFormFragment::class.java, + FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + ) + } + } + + private fun openWaitForEmailVerification(email: String) { + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + when { + vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( + FtueAuthWaitForEmailFragment::class.java, + FtueAuthWaitForEmailFragmentArgument(email), + ) + else -> addRegistrationStageFragmentToBackstack( + FtueAuthLegacyWaitForEmailFragment::class.java, + FtueAuthWaitForEmailFragmentArgument(email), + ) + } + } + private fun onTerms(stage: Stage.Terms) { when { vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index d78e0fe74d..c81a9c2feb 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -21,13 +21,16 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.app.R -import im.vector.app.databinding.FragmentLoginWaitForEmailBinding +import im.vector.app.core.utils.colorTerminatingFullStop +import im.vector.app.databinding.FragmentFtueWaitForEmailVerificationBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.RegisterAction +import im.vector.app.features.themes.ThemeProvider +import im.vector.app.features.themes.ThemeUtils import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject @Parcelize @@ -38,45 +41,57 @@ data class FtueAuthWaitForEmailFragmentArgument( /** * In this screen, the user is asked to check their emails. */ -class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragment() { +class FtueAuthWaitForEmailFragment @Inject constructor( + private val themeProvider: ThemeProvider +) : AbstractFtueAuthFragment() { private val params: FtueAuthWaitForEmailFragmentArgument by args() + private var inferHasLeftAndReturnedToScreen = false - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginWaitForEmailBinding { - return FragmentLoginWaitForEmailBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueWaitForEmailVerificationBinding { + return FragmentFtueWaitForEmailVerificationBinding.inflate(inflater, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupUi() } + private fun setupUi() { + views.emailVerificationGradientContainer.setBackgroundResource( + when (themeProvider.isLightTheme()) { + true -> R.drawable.bg_waiting_for_email_verification + false -> R.drawable.bg_color_background + } + ) + views.emailVerificationTitle.text = getString(R.string.ftue_auth_email_verification_title) + .colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary)) + views.emailVerificationSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email) + views.emailVerificationResendEmail.debouncedClicks { + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid)) + } + } + override fun onResume() { super.onResume() - + showLoadingIfReturningToScreen() viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) } + private fun showLoadingIfReturningToScreen() { + when (inferHasLeftAndReturnedToScreen) { + true -> views.emailVerificationWaiting.isVisible = true + false -> { + inferHasLeftAndReturnedToScreen = true + } + } + } + override fun onPause() { super.onPause() - viewModel.handle(OnboardingAction.StopEmailValidationCheck) } - private fun setupUi() { - views.loginWaitForEmailNotice.text = getString(R.string.login_wait_for_email_notice, params.email) - } - - override fun onError(throwable: Throwable) { - if (throwable.is401()) { - // Try again, with a delay - viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000))) - } else { - super.onError(throwable) - } - } - override fun resetViewModel() { viewModel.handle(OnboardingAction.ResetAuthenticationAttempt) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt index 23f7014374..f8b885ddee 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt @@ -23,11 +23,11 @@ import im.vector.app.R import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.isEnglishSpeaking +import im.vector.app.core.utils.colorTerminatingFullStop import im.vector.app.features.themes.ThemeProvider import im.vector.app.features.themes.ThemeUtils import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence -import me.gujun.android.span.span import javax.inject.Inject class SplashCarouselStateFactory @Inject constructor( @@ -39,7 +39,7 @@ class SplashCarouselStateFactory @Inject constructor( fun create(): SplashCarouselState { val lightTheme = themeProvider.isLightTheme() - fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_carousel_page_dark + fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_color_background fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable return SplashCarouselState( listOf( @@ -79,18 +79,8 @@ class SplashCarouselStateFactory @Inject constructor( } private fun Int.colorTerminatingFullStop(@AttrRes color: Int): EpoxyCharSequence { - val string = stringProvider.getString(this) - val fullStop = "." - val charSequence = if (string.endsWith(fullStop)) { - span { - +string.removeSuffix(fullStop) - span(fullStop) { - textColor = ThemeUtils.getColor(context, color) - } - } - } else { - string - } - return charSequence.toEpoxyCharSequence() + return stringProvider.getString(this) + .colorTerminatingFullStop(ThemeUtils.getColor(context, color)) + .toEpoxyCharSequence() } } diff --git a/vector/src/main/res/drawable/ic_email.xml b/vector/src/main/res/drawable/ic_email.xml new file mode 100644 index 0000000000..48de7aec41 --- /dev/null +++ b/vector/src/main/res/drawable/ic_email.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_email_input.xml b/vector/src/main/res/layout/fragment_ftue_email_input.xml new file mode 100644 index 0000000000..0cfcfea7cc --- /dev/null +++ b/vector/src/main/res/layout/fragment_ftue_email_input.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +