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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_wait_for_email_verification.xml b/vector/src/main/res/layout/fragment_ftue_wait_for_email_verification.xml
new file mode 100644
index 0000000000..15aaf4d1b2
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_ftue_wait_for_email_verification.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/values/donottranslate.xml b/vector/src/main/res/values/donottranslate.xml
index 221a80e6b3..2dcd36d7c8 100755
--- a/vector/src/main/res/values/donottranslate.xml
+++ b/vector/src/main/res/values/donottranslate.xml
@@ -31,4 +31,13 @@
Privacy policy
Please read through T&C. You must accept in order to continue.
+ Enter your email address
+ This will help verify your account and enables password recovery.
+ Email Address
+
+ Check your email to verify.
+
+ To confirm your email address, tap the button in the email we just sent to %s
+ Did not receive an email?
+ Resend email
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index 59f6d4ea12..ac8a4c364e 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -46,8 +46,6 @@ import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
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.Stage
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@@ -57,10 +55,10 @@ private const val A_PICTURE_FILENAME = "a-picture.png"
private val AN_ERROR = RuntimeException("an error!")
private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
-private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(RegisterThreePid.Email("an email"))
+private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
-private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT)
+private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT)
private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
@@ -230,7 +228,7 @@ class OnboardingViewModelTest {
@Test
fun `given register action ignores result, when handling action, then does nothing on success`() = runTest {
val test = viewModel.test()
- givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT))
+ givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT))
viewModel.handle(OnboardingAction.PostRegisterAction(A_RESULT_IGNORED_REGISTER_ACTION))
@@ -249,7 +247,7 @@ class OnboardingViewModelTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
- givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT))
+ givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT))
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
val test = viewModel.test()
@@ -291,7 +289,7 @@ class OnboardingViewModelTest {
@Test
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
fakeVectorFeatures.givenPersonalisationEnabled()
- givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Success(fakeSession))
+ givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Complete(fakeSession))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
val test = viewModel.test()
@@ -495,8 +493,8 @@ class OnboardingViewModelTest {
val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList())
givenRegistrationResultsFor(
listOf(
- A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult),
- RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession)
+ A_LOADABLE_REGISTER_ACTION to RegistrationResult.NextStep(flowResult),
+ RegisterAction.RegisterDummy to RegistrationResult.Complete(fakeSession)
)
)
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt
index a7fa2a6331..f6d9317038 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt
@@ -18,16 +18,19 @@ package im.vector.app.features.onboarding
import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fixtures.a401ServerError
import io.mockk.coVerifyAll
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
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.RegistrationWizard
+import org.matrix.android.sdk.api.auth.registration.RegistrationResult as SdkResult
+private const val IGNORED_DELAY = 0L
+private val AN_ERROR = RuntimeException()
private val A_SESSION = FakeSession()
-private val AN_EXPECTED_RESULT = RegistrationResult.Success(A_SESSION)
+private val AN_EXPECTED_RESULT = RegistrationResult.Complete(A_SESSION)
private const val A_USERNAME = "a username"
private const val A_PASSWORD = "a password"
private const val AN_INITIAL_DEVICE_NAME = "a device name"
@@ -38,6 +41,9 @@ private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email")
class RegistrationActionHandlerTest {
+ private val fakeRegistrationWizard = FakeRegistrationWizard()
+ private val registrationActionHandler = RegistrationActionHandler()
+
@Test
fun `when handling register action then delegates to wizard`() = runTest {
val cases = listOf(
@@ -57,9 +63,52 @@ class RegistrationActionHandlerTest {
cases.forEach { testSuccessfulActionDelegation(it) }
}
+ @Test
+ fun `given adding an email ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest {
+ fakeRegistrationWizard.givenAddEmailThreePidErrors(
+ cause = a401ServerError(),
+ email = A_PID_TO_REGISTER.email
+ )
+
+ val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.AddThreePid(A_PID_TO_REGISTER))
+
+ result shouldBeEqualTo RegistrationResult.SendEmailSuccess(A_PID_TO_REGISTER.email)
+ }
+
+ @Test
+ fun `given email verification errors with 401 then fatal error, when checking email validation, then continues to poll until non 401 error`() = runTest {
+ val errorsToThrow = listOf(
+ a401ServerError(),
+ a401ServerError(),
+ a401ServerError(),
+ AN_ERROR
+ )
+ fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow)
+
+ val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY))
+
+ fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size)
+ result shouldBeEqualTo RegistrationResult.Error(AN_ERROR)
+ }
+
+ @Test
+ fun `given email verification errors with 401 and succeeds, when checking email validation, then continues to poll until success`() = runTest {
+ val errorsToThrow = listOf(
+ a401ServerError(),
+ a401ServerError(),
+ a401ServerError()
+ )
+ fakeRegistrationWizard.givenCheckIfEmailHasBeenValidatedErrors(errorsToThrow, finally = SdkResult.Success(A_SESSION))
+
+ val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, RegisterAction.CheckIfEmailHasBeenValidated(IGNORED_DELAY))
+
+ fakeRegistrationWizard.verifyCheckedEmailedVerification(times = errorsToThrow.size + 1)
+ result shouldBeEqualTo RegistrationResult.Complete(A_SESSION)
+ }
+
private suspend fun testSuccessfulActionDelegation(case: Case) {
- val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard()
+ val registrationActionHandler = RegistrationActionHandler()
fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect)
val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action)
@@ -69,6 +118,6 @@ class RegistrationActionHandlerTest {
}
}
-private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> RegistrationResult) = Case(action, expect)
+private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> SdkResult) = Case(action, expect)
-private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> RegistrationResult)
+private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> SdkResult)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt
index 61d0e438ab..f5824e5866 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt
@@ -18,9 +18,9 @@ package im.vector.app.test.fakes
import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.RegistrationActionHandler
+import im.vector.app.features.onboarding.RegistrationResult
import io.mockk.coEvery
import io.mockk.mockk
-import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
class FakeRegisterActionHandler {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
index 4e6e511abb..0fc69ae995 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt
@@ -17,7 +17,9 @@
package im.vector.app.test.fakes
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.mockk
+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.RegistrationWizard
import org.matrix.android.sdk.api.session.Session
@@ -27,4 +29,21 @@ class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) {
coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
}
+
+ fun givenAddEmailThreePidErrors(cause: Throwable, email: String) {
+ coEvery { addThreePid(RegisterThreePid.Email(email)) } throws cause
+ }
+
+ fun givenCheckIfEmailHasBeenValidatedErrors(errors: List, finally: RegistrationResult? = null) {
+ var index = 0
+ coEvery { checkIfEmailHasBeenValidated(any()) } answers {
+ val current = index
+ index++
+ errors.getOrNull(current)?.let { throw it } ?: finally ?: throw RuntimeException("Developer error")
+ }
+ }
+
+ fun verifyCheckedEmailedVerification(times: Int) {
+ coVerify(exactly = times) { checkIfEmailHasBeenValidated(any()) }
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt
new file mode 100644
index 0000000000..39c139c208
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.fixtures
+
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.MatrixError
+import javax.net.ssl.HttpsURLConnection
+
+fun a401ServerError() = Failure.ServerError(
+ MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED
+)