adding email input FTUE screen

- lifts the threepid email error handling to the RegistrationActionHandler rather than having the UI infer success from a 401
This commit is contained in:
Adam Brown 2022-04-26 17:35:42 +01:00
parent 4094a66f3c
commit d4a5b71a4d
16 changed files with 400 additions and 39 deletions

View File

@ -101,6 +101,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment 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.FtueAuthGenericTextInputFormFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
@ -494,6 +495,11 @@ interface FragmentModule {
@FragmentKey(FtueAuthAccountCreatedFragment::class) @FragmentKey(FtueAuthAccountCreatedFragment::class)
fun bindFtueAuthAccountCreatedFragment(fragment: FtueAuthAccountCreatedFragment): Fragment fun bindFtueAuthAccountCreatedFragment(fragment: FtueAuthAccountCreatedFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthEmailEntryFragment::class)
fun bindFtueAuthEmailEntryFragment(fragment: FtueAuthEmailEntryFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(FtueAuthChooseDisplayNameFragment::class) @FragmentKey(FtueAuthChooseDisplayNameFragment::class)

View File

@ -16,7 +16,11 @@
package im.vector.app.core.extensions 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 com.google.android.material.textfield.TextInputLayout
import im.vector.app.core.platform.SimpleTextWatcher
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import reactivecircus.flowbinding.android.widget.textChanges 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.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() }
fun TextInputLayout.content() = editText().text.toString() 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.setOnImeDone(action: () -> Unit) {
editText?.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE -> {
action()
true
}
else -> false
}
}
}

View File

@ -50,7 +50,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.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.login.LoginWizard 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.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.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -275,8 +274,10 @@ class OnboardingViewModel @AssistedInject constructor(
// do nothing // do nothing
} }
else -> when (it) { else -> when (it) {
is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true) is RegistrationResult.Complete -> onSessionCreated(it.session, isAccountCreated = true)
is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult, onNextRegistrationStepAction) 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))
} }
} }
}, },

View File

@ -16,26 +16,73 @@
package im.vector.app.features.onboarding 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.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.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.failure.is401
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
import org.matrix.android.sdk.api.auth.registration.RegistrationResult as SdkRegistrationResult
class RegistrationActionHandler @Inject constructor() { class RegistrationActionHandler @Inject constructor() {
suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult {
return when (action) { return when (action) {
RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow() RegisterAction.StartRegistration -> resultOf { registrationWizard.getRegistrationFlow() }
is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse) is RegisterAction.CaptchaDone -> resultOf { registrationWizard.performReCaptcha(action.captchaResponse) }
is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms() is RegisterAction.AcceptTerms -> resultOf { registrationWizard.acceptTerms() }
is RegisterAction.RegisterDummy -> registrationWizard.dummy() is RegisterAction.RegisterDummy -> resultOf { registrationWizard.dummy() }
is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid) is RegisterAction.AddThreePid -> handleAddThreePid(registrationWizard, action)
is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid() is RegisterAction.SendAgainThreePid -> resultOf { registrationWizard.sendAgainThreePid() }
is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code) is RegisterAction.ValidateThreePid -> resultOf { registrationWizard.handleValidateThreePid(action.code) }
is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) is RegisterAction.CheckIfEmailHasBeenValidated -> resultOf { registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) }
is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName) 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 = {
when (it) {
is Success -> RegistrationResult.Complete(it.session)
is FlowResponse -> RegistrationResult.NextStep(it.flowResult)
}
},
onFailure = {
when {
action.threePid is RegisterThreePid.Email && it.is401() -> RegistrationResult.SendEmailSuccess(action.threePid.email)
else -> RegistrationResult.Error(it)
}
}
)
}
}
private inline fun resultOf(block: () -> SdkRegistrationResult): RegistrationResult {
return runCatching { block() }.fold(
onSuccess = {
when (it) {
is FlowResponse -> RegistrationResult.NextStep(it.flowResult)
is Success -> RegistrationResult.Complete(it.session)
}
},
onFailure = { RegistrationResult.Error(it) }
)
}
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 { sealed interface RegisterAction {
@ -56,7 +103,6 @@ sealed interface RegisterAction {
} }
fun RegisterAction.ignoresResult() = when (this) { fun RegisterAction.ignoresResult() = when (this) {
is RegisterAction.AddThreePid -> true
is RegisterAction.SendAgainThreePid -> true is RegisterAction.SendAgainThreePid -> true
else -> false else -> false
} }

View File

@ -23,6 +23,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import com.google.android.material.textfield.TextInputLayout 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.core.platform.SimpleTextWatcher
import im.vector.app.databinding.FragmentFtueDisplayNameBinding import im.vector.app.databinding.FragmentFtueDisplayNameBinding
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
@ -69,7 +70,7 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {
views.displayNameInput.editText?.setText(state.personalizationState.displayName) views.displayNameInput.editText?.setText(state.personalizationState.displayName)
views.displayNameSubmit.isEnabled = views.displayNameInput.hasContentEmpty() views.displayNameSubmit.isEnabled = views.displayNameInput.hasContent()
} }
override fun resetViewModel() { override fun resetViewModel() {
@ -81,5 +82,3 @@ class FtueAuthChooseDisplayNameFragment @Inject constructor() : AbstractFtueAuth
return true return true
} }
} }
private fun TextInputLayout.hasContentEmpty() = !editText?.text.isNullOrEmpty()

View File

@ -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.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.hasContent
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.setOnImeDone
import im.vector.app.databinding.FragmentFtueEmailInputBinding
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
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 org.matrix.android.sdk.api.failure.is401
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject
class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtueEmailInputBinding>() {
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.setOnImeDone { updateEmail() }
views.emailEntrySubmit.debouncedClicks { updateEmail() }
views.emailEntryInput.editText().textChanges()
.onEach {
views.emailEntryInput.error = null
views.emailEntrySubmit.isEnabled = it.isEmail()
}
.launchIn(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 updateWithState(state: OnboardingViewState) {
views.emailEntrySubmit.isEnabled = views.emailEntryInput.content().isEmail()
}
override fun resetViewModel() {
// Nothing to do
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
return true
}
}

View File

@ -223,12 +223,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
override fun onError(throwable: Throwable) { override fun onError(throwable: Throwable) {
when (params.mode) { when (params.mode) {
TextInputFormFragmentMode.SetEmail -> { TextInputFormFragmentMode.SetEmail -> {
if (throwable.is401()) { views.loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
// 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)
}
} }
TextInputFormFragmentMode.SetMsisdn -> { TextInputFormFragmentMode.SetMsisdn -> {
if (throwable.is401()) { if (throwable.is401()) {

View File

@ -393,10 +393,7 @@ class FtueAuthVariant(
when (stage) { when (stage) {
is Stage.ReCaptcha -> onCaptcha(stage) is Stage.ReCaptcha -> onCaptcha(stage)
is Stage.Email -> addRegistrationStageFragmentToBackstack( is Stage.Email -> onEmail(stage)
FtueAuthGenericTextInputFormFragment::class.java,
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory),
)
is Stage.Msisdn -> addRegistrationStageFragmentToBackstack( is Stage.Msisdn -> addRegistrationStageFragmentToBackstack(
FtueAuthGenericTextInputFormFragment::class.java, FtueAuthGenericTextInputFormFragment::class.java,
FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), FtueAuthGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory),
@ -406,6 +403,18 @@ 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 onTerms(stage: Stage.Terms) { private fun onTerms(stage: Stage.Terms) {
when { when {
vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack( vectorFeatures.isOnboardingCombinedRegisterEnabled() -> addRegistrationStageFragmentToBackstack(

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="71dp"
android:height="70dp"
android:viewportWidth="71"
android:viewportHeight="70">
<path
android:pathData="M16.261,23.576L34.905,42.161C35.545,42.799 36.581,42.799 37.221,42.161L55.773,23.667C55.92,24.084 56,24.533 56,25V46C56,48.209 54.209,50 52,50H20C17.791,50 16,48.209 16,46V25C16,24.498 16.092,24.018 16.261,23.576ZM18.582,21.258C19.023,21.091 19.501,21 20,21H52C52.533,21 53.042,21.104 53.508,21.294L36.063,38.684L18.582,21.258Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/emailEntryGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/emailEntryGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderIcon"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/emailEntryHeaderIcon"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:contentDescription="@null"
android:src="@drawable/ic_email"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/emailEntryHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_email_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/emailEntryHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderIcon" />
<TextView
android:id="@+id/emailEntryHeaderSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/ftue_auth_email_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/emailEntryInput"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/emailEntryHeaderSubtitle" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/emailEntryInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/ftue_auth_email_entry_title"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textEmailAddress"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:id="@+id/entrySpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/emailEntrySubmit"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/emailEntryInput"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="@+id/emailEntrySubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_set_email_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
app:layout_constraintStart_toStartOf="@id/emailEntryGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -31,4 +31,8 @@
<string name="ftue_auth_terms_title">Privacy policy</string> <string name="ftue_auth_terms_title">Privacy policy</string>
<string name="ftue_auth_terms_subtitle">Please read through T&amp;C. You must accept in order to continue.</string> <string name="ftue_auth_terms_subtitle">Please read through T&amp;C. You must accept in order to continue.</string>
<string name="ftue_auth_email_title">Enter your email address</string>
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
<string name="ftue_auth_email_entry_title">Email Address</string>
</resources> </resources>

View File

@ -47,7 +47,6 @@ import org.junit.Test
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig 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.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid 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.auth.registration.Stage
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@ -60,7 +59,7 @@ private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenV
private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(RegisterThreePid.Email("an email")) private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(RegisterThreePid.Email("an email"))
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true) private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList()) 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 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 const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
@ -230,7 +229,7 @@ class OnboardingViewModelTest {
@Test @Test
fun `given register action ignores result, when handling action, then does nothing on success`() = runTest { fun `given register action ignores result, when handling action, then does nothing on success`() = runTest {
val test = viewModel.test() 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)) viewModel.handle(OnboardingAction.PostRegisterAction(A_RESULT_IGNORED_REGISTER_ACTION))
@ -249,7 +248,7 @@ class OnboardingViewModelTest {
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG) fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE)) 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()) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
val test = viewModel.test() val test = viewModel.test()
@ -291,7 +290,7 @@ class OnboardingViewModelTest {
@Test @Test
fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest { fun `given personalisation enabled, when registering account, then updates state and emits account created event`() = runTest {
fakeVectorFeatures.givenPersonalisationEnabled() fakeVectorFeatures.givenPersonalisationEnabled()
givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Success(fakeSession)) givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Complete(fakeSession))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
val test = viewModel.test() val test = viewModel.test()
@ -495,8 +494,8 @@ class OnboardingViewModelTest {
val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList()) val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList())
givenRegistrationResultsFor( givenRegistrationResultsFor(
listOf( listOf(
A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult), A_LOADABLE_REGISTER_ACTION to RegistrationResult.NextStep(flowResult),
RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession) RegisterAction.RegisterDummy to RegistrationResult.Complete(fakeSession)
) )
) )
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES) givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)

View File

@ -18,16 +18,17 @@ package im.vector.app.features.onboarding
import im.vector.app.test.fakes.FakeRegistrationWizard import im.vector.app.test.fakes.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fixtures.a401ServerError
import io.mockk.coVerifyAll import io.mockk.coVerifyAll
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid 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.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationResult as SdkResult
private val A_SESSION = FakeSession() 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_USERNAME = "a username"
private const val A_PASSWORD = "a password" private const val A_PASSWORD = "a password"
private const val AN_INITIAL_DEVICE_NAME = "a device name" private const val AN_INITIAL_DEVICE_NAME = "a device name"
@ -57,6 +58,20 @@ class RegistrationActionHandlerTest {
cases.forEach { testSuccessfulActionDelegation(it) } cases.forEach { testSuccessfulActionDelegation(it) }
} }
@Test
fun `given adding an email ThreePid fails with 401, when handling register action, then infer EmailSuccess`() = runTest {
val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard()
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)
}
private suspend fun testSuccessfulActionDelegation(case: Case) { private suspend fun testSuccessfulActionDelegation(case: Case) {
val registrationActionHandler = RegistrationActionHandler() val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard() val fakeRegistrationWizard = FakeRegistrationWizard()
@ -69,6 +84,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)

View File

@ -18,9 +18,9 @@ package im.vector.app.test.fakes
import im.vector.app.features.onboarding.RegisterAction import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.RegistrationActionHandler import im.vector.app.features.onboarding.RegistrationActionHandler
import im.vector.app.features.onboarding.RegistrationResult
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
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.RegistrationWizard
class FakeRegisterActionHandler { class FakeRegisterActionHandler {

View File

@ -18,6 +18,7 @@ package im.vector.app.test.fakes
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk 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.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -27,4 +28,8 @@ class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) { fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) {
coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result) coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
} }
fun givenAddEmailThreePidErrors(cause: Throwable, email: String) {
coEvery { addThreePid(RegisterThreePid.Email(email)) } throws cause
}
} }

View File

@ -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
)