Merge pull request #5648 from vector-im/feature/adm/ftue-sign-up

FTUE - Combined sign up + server selection screen
This commit is contained in:
Adam Brown 2022-04-01 13:51:35 +01:00 committed by GitHub
commit 27b727ed41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 632 additions and 28 deletions

1
changelog.d/5277.wip Normal file
View file

@ -0,0 +1 @@
Adding combined account creation and server selection screen as part of the new FTUE

View file

@ -9,6 +9,11 @@
<item name="endIconTint">?vctr_content_secondary</item> <item name="endIconTint">?vctr_content_secondary</item>
</style> </style>
<style name="Widget.Vector.TextInputLayout.Username">
<item name="endIconMode">clear_text</item>
<item name="endIconTint">?vctr_content_secondary</item>
</style>
<style name="Widget.Vector.TextInputLayout.Form"> <style name="Widget.Vector.TextInputLayout.Form">
<item name="boxStrokeColor">@color/form_edit_text_stroke_color_selector</item> <item name="boxStrokeColor">@color/form_edit_text_stroke_color_selector</item>
<item name="android:textColorHint">@color/form_edit_text_hint_color_selector</item> <item name="android:textColorHint">@color/form_edit_text_hint_color_selector</item>

View file

@ -54,6 +54,11 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.onboardingPersonalize, key = DebugFeatureKeys.onboardingPersonalize,
factory = VectorFeatures::isOnboardingPersonalizeEnabled factory = VectorFeatures::isOnboardingPersonalizeEnabled
), ),
createBooleanFeature(
label = "FTUE Combined register",
key = DebugFeatureKeys.onboardingCombinedRegister,
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
),
createBooleanFeature( createBooleanFeature(
label = "Live location sharing", label = "Live location sharing",
key = DebugFeatureKeys.liveLocationSharing, key = DebugFeatureKeys.liveLocationSharing,

View file

@ -54,6 +54,9 @@ class DebugVectorFeatures(
override fun isOnboardingPersonalizeEnabled(): Boolean = read(DebugFeatureKeys.onboardingPersonalize) override fun isOnboardingPersonalizeEnabled(): Boolean = read(DebugFeatureKeys.onboardingPersonalize)
?: vectorFeatures.isOnboardingPersonalizeEnabled() ?: vectorFeatures.isOnboardingPersonalizeEnabled()
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
override fun isLiveLocationEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing) override fun isLiveLocationEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing)
?: vectorFeatures.isLiveLocationEnabled() ?: vectorFeatures.isLiveLocationEnabled()
@ -107,7 +110,8 @@ private fun <T : Enum<T>> enumPreferencesKey(type: KClass<T>) = stringPreference
object DebugFeatureKeys { object DebugFeatureKeys {
val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account") val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account")
val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel") val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel")
val onboardingUseCase = booleanPreferencesKey("onbboarding-splash-carousel") val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
val onboardingPersonalize = booleanPreferencesKey("onbboarding-personalize") val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
val liveLocationSharing = booleanPreferencesKey("live-location-sharing") val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
} }

View file

@ -16,8 +16,12 @@
package im.vector.app.core.extensions package im.vector.app.core.extensions
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.children
import androidx.core.view.doOnLayout
import kotlin.math.roundToInt
fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) { fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) {
ConstraintSet().let { ConstraintSet().let {
@ -26,3 +30,21 @@ fun ConstraintLayout.updateConstraintSet(block: (ConstraintSet) -> Unit) {
it.applyTo(this) it.applyTo(this)
} }
} }
/**
* Helper to recalculate all ConstraintLayout child views with percentage based height against the parent's height.
* This is helpful when using a ConstraintLayout within a ScrollView as any percentages will use the total scrolling size
* instead of the viewport/ScrollView height
*/
fun ConstraintLayout.realignPercentagesToParent() {
doOnLayout {
val rootHeight = (parent as View).height
children.forEach { child ->
val params = child.layoutParams as ConstraintLayout.LayoutParams
if (params.matchConstraintPercentHeight != 1.0f) {
params.height = (rootHeight * params.matchConstraintPercentHeight).roundToInt()
child.layoutParams = params
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* 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 com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.flow.map
import reactivecircus.flowbinding.android.widget.textChanges
fun TextInputLayout.editText() = this.editText!!
/**
* Detect if a field starts or ends with spaces
*/
fun TextInputLayout.hasSurroundingSpaces() = editText().text.toString().let { it.trim() != it }
fun TextInputLayout.hasContentFlow(mapper: (CharSequence) -> CharSequence = { it }) = editText().textChanges().map { mapper(it).isNotEmpty() }
fun TextInputLayout.content() = editText().text.toString()

View file

@ -25,6 +25,7 @@ interface VectorFeatures {
fun isOnboardingSplashCarouselEnabled(): Boolean fun isOnboardingSplashCarouselEnabled(): Boolean
fun isOnboardingUseCaseEnabled(): Boolean fun isOnboardingUseCaseEnabled(): Boolean
fun isOnboardingPersonalizeEnabled(): Boolean fun isOnboardingPersonalizeEnabled(): Boolean
fun isOnboardingCombinedRegisterEnabled(): Boolean
fun isLiveLocationEnabled(): Boolean fun isLiveLocationEnabled(): Boolean
enum class OnboardingVariant { enum class OnboardingVariant {
@ -40,5 +41,6 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingSplashCarouselEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingPersonalizeEnabled() = false
override fun isOnboardingCombinedRegisterEnabled() = false
override fun isLiveLocationEnabled(): Boolean = BuildConfig.ENABLE_LIVE_LOCATION_SHARING override fun isLiveLocationEnabled(): Boolean = BuildConfig.ENABLE_LIVE_LOCATION_SHARING
} }

View file

@ -125,7 +125,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
error++ error++
} }
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = "The homeserver does not accept username with only digits." views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
error++ error++
} }
if (password.isEmpty()) { if (password.isEmpty()) {

View file

@ -30,7 +30,7 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
LinearLayout(context, attrs, defStyle) { LinearLayout(context, attrs, defStyle) {
interface InteractionListener { fun interface InteractionListener {
fun onProviderSelected(id: String?) fun onProviderSelected(id: String?)
} }

View file

@ -41,6 +41,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
// Login or Register, depending on the signMode // Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
object StopEmailValidationCheck : OnboardingAction object StopEmailValidationCheck : OnboardingAction
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
@ -51,7 +52,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
object ResetHomeServerType : ResetAction object ResetHomeServerType : ResetAction
object ResetHomeServerUrl : ResetAction object ResetHomeServerUrl : ResetAction
object ResetSignMode : ResetAction object ResetSignMode : ResetAction
object ResetLogin : ResetAction object ResetAuthenticationAttempt : ResetAction
object ResetResetPassword : ResetAction object ResetResetPassword : ResetAction
// Homeserver history // Homeserver history

View file

@ -36,6 +36,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OpenUseCaseSelection : OnboardingViewEvents() object OpenUseCaseSelection : OnboardingViewEvents()
object OpenServerSelection : OnboardingViewEvents() object OpenServerSelection : OnboardingViewEvents()
object OpenCombinedRegister : OnboardingViewEvents()
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
object OnLoginFlowRetrieved : OnboardingViewEvents() object OnLoginFlowRetrieved : OnboardingViewEvents()
data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents() data class OnSignModeSelected(val signMode: SignMode) : OnboardingViewEvents()

View file

@ -141,6 +141,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.InitWith -> handleInitWith(action) is OnboardingAction.InitWith -> handleInitWith(action)
is OnboardingAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } is OnboardingAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPassword -> handleResetPassword(action)
@ -276,7 +277,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { private fun handleRegisterWith(action: OnboardingAction.Register) {
reAuthHelper.data = action.password reAuthHelper.data = action.password
handleRegisterAction(RegisterAction.CreateAccount( handleRegisterAction(RegisterAction.CreateAccount(
action.username, action.username,
@ -322,7 +323,7 @@ class OnboardingViewModel @AssistedInject constructor(
) )
} }
} }
OnboardingAction.ResetLogin -> { OnboardingAction.ResetAuthenticationAttempt -> {
viewModelScope.launch { viewModelScope.launch {
authenticationService.cancelPendingLoginOrRegistration() authenticationService.cancelPendingLoginOrRegistration()
setState { copy(isLoading = false) } setState { copy(isLoading = false) }
@ -356,7 +357,13 @@ class OnboardingViewModel @AssistedInject constructor(
private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) { private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
setState { copy(useCase = action.useCase) } setState { copy(useCase = action.useCase) }
_viewEvents.post(OnboardingViewEvents.OpenServerSelection) when (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
true -> {
handle(OnboardingAction.UpdateHomeServer(matrixOrgUrl))
OnboardingViewEvents.OpenCombinedRegister
}
false -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
}
} }
private fun resetUseCase() { private fun resetUseCase() {
@ -459,7 +466,7 @@ class OnboardingViewModel @AssistedInject constructor(
when (state.signMode) { when (state.signMode) {
SignMode.Unknown -> error("Developer error, invalid sign mode") SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action) SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action) SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName))
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null) SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -191,7 +191,7 @@ class FtueAuthCaptchaFragment @Inject constructor(
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {

View file

@ -0,0 +1,206 @@
/*
* Copyright 2019 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.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants
import androidx.core.text.isDigitsOnly
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.hasContentFlow
import im.vector.app.core.extensions.hasSurroundingSpaces
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.databinding.FragmentFtueSignUpCombinedBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
import org.matrix.android.sdk.api.failure.isRegistrationDisabled
import org.matrix.android.sdk.api.failure.isUsernameInUse
import org.matrix.android.sdk.api.failure.isWeakPassword
import javax.inject.Inject
class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentFtueSignUpCombinedBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueSignUpCombinedBinding {
return FragmentFtueSignUpCombinedBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
views.createAccountRoot.realignPercentagesToParent()
views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupSubmitButton() {
views.createAccountSubmit.setOnClickListener { submit() }
observeInputFields()
.onEach {
views.createAccountPasswordInput.error = null
views.createAccountInput.error = null
views.createAccountSubmit.isEnabled = it
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun observeInputFields() = combine(
views.createAccountInput.hasContentFlow { it.trim() },
views.createAccountPasswordInput.hasContentFlow(),
transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty }
)
private fun submit() {
withState(viewModel) { state ->
cleanupUi()
val login = views.createAccountInput.content()
val password = views.createAccountPasswordInput.content()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.createAccountInput.error = getString(R.string.error_empty_field_choose_user_name)
error++
}
if (state.isNumericOnlyUserIdForbidden() && login.isDigitsOnly()) {
views.createAccountInput.error = getString(R.string.error_forbidden_digits_only_username)
error++
}
if (password.isEmpty()) {
views.createAccountPasswordInput.error = getString(R.string.error_empty_field_choose_password)
error++
}
if (error == 0) {
viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name)))
}
}
}
private fun cleanupUi() {
views.createAccountSubmit.hideKeyboard()
views.createAccountInput.error = null
views.createAccountPasswordInput.error = null
}
override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
}
override fun onError(throwable: Throwable) {
// Trick to display the error without text.
views.createAccountInput.error = " "
when {
throwable.isUsernameInUse() || throwable.isInvalidUsername() -> {
views.createAccountInput.error = errorFormatter.toHumanReadable(throwable)
}
throwable.isLoginEmailUnknown() -> {
views.createAccountInput.error = getString(R.string.login_login_with_email_error)
}
throwable.isInvalidPassword() && views.createAccountPasswordInput.hasSurroundingSpaces() -> {
views.createAccountPasswordInput.error = getString(R.string.auth_invalid_login_param_space_in_password)
}
throwable.isWeakPassword() || throwable.isInvalidPassword() -> {
views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable)
}
throwable.isRegistrationDisabled() -> {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.login_registration_disabled))
.setPositiveButton(R.string.ok, null)
.show()
}
else -> {
super.onError(throwable)
}
}
}
override fun updateWithState(state: OnboardingViewState) {
setupUi(state)
setupAutoFill()
if (state.isLoading) {
// Ensure password is hidden
views.createAccountPasswordInput.editText().hidePassword()
}
}
private fun setupUi(state: OnboardingViewState) {
when (state.loginMode) {
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.loginMode.ssoIdentityProviders)
else -> hideSsoProviders()
}
}
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE
views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted()
views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id ->
viewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,
providerId = id
)?.let { openInCustomTab(it) }
}
}
private fun hideSsoProviders() {
views.ssoGroup.isVisible = false
views.ssoButtons.ssoIdentityProviders = null
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.createAccountInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
views.createAccountPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
}
}
private fun OnboardingViewState.isNumericOnlyUserIdForbidden() = serverType == ServerType.MatrixOrg

View file

@ -254,6 +254,6 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
} }

View file

@ -135,7 +135,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
error++ error++
} }
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = "The homeserver does not accept username with only digits." views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
error++ error++
} }
if (password.isEmpty()) { if (password.isEmpty()) {
@ -254,7 +254,7 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
override fun onError(throwable: Throwable) { override fun onError(throwable: Throwable) {

View file

@ -136,6 +136,9 @@ class FtueAuthVariant(
if (viewEvents.isRegistrationStarted) { if (viewEvents.isRegistrationStarted) {
// Go on with registration flow // Go on with registration flow
handleRegistrationNavigation(viewEvents.flowResult) handleRegistrationNavigation(viewEvents.flowResult)
} else {
if (vectorFeatures.isOnboardingCombinedRegisterEnabled()) {
openCombinedRegister()
} else { } else {
// First ask for login and password // First ask for login and password
// I add a tag to indicate that this fragment is a registration stage. // I add a tag to indicate that this fragment is a registration stage.
@ -144,6 +147,7 @@ class FtueAuthVariant(
} }
} }
} }
}
is OnboardingViewEvents.OutdatedHomeserver -> { is OnboardingViewEvents.OutdatedHomeserver -> {
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.login_error_outdated_homeserver_title) .setTitle(R.string.login_error_outdated_homeserver_title)
@ -221,6 +225,7 @@ class FtueAuthVariant(
FtueAuthUseCaseFragment::class.java, FtueAuthUseCaseFragment::class.java,
option = commonOption) option = commonOption)
} }
OnboardingViewEvents.OpenCombinedRegister -> openCombinedRegister()
is OnboardingViewEvents.OnAccountCreated -> onAccountCreated() is OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn() OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn()
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName() OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
@ -231,6 +236,15 @@ class FtueAuthVariant(
} }
} }
private fun openCombinedRegister() {
activity.addFragmentToBackstack(
views.loginFragmentContainer,
FtueAuthCombinedRegisterFragment::class.java,
tag = FRAGMENT_REGISTRATION_STAGE_TAG,
option = commonOption
)
}
private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) = private fun registrationShouldFallback(registrationFlowResult: OnboardingViewEvents.RegistrationFlowResult) =
isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow() isForceLoginFallbackEnabled || registrationFlowResult.containsUnsupportedRegistrationFlow()

View file

@ -78,6 +78,6 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
} }

View file

@ -235,7 +235,7 @@ class FtueAuthWebFragment @Inject constructor(
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
override fun onBackPressed(toolbarButton: Boolean): Boolean { override fun onBackPressed(toolbarButton: Boolean): Boolean {

View file

@ -121,6 +121,6 @@ class FtueAuthTermsFragment @Inject constructor(
} }
override fun resetViewModel() { override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetLogin) viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
} }
} }

View file

@ -114,7 +114,7 @@
app:layout_constraintTop_toBottomOf="@id/displayNameInput" /> app:layout_constraintTop_toBottomOf="@id/displayNameInput" />
<Space <Space
android:id="@+id/actionsSpacing" android:id="@+id/entrySpacing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/displayNameSubmit" app:layout_constraintBottom_toTopOf="@id/displayNameSubmit"
@ -133,7 +133,7 @@
app:layout_constraintBottom_toTopOf="@id/displayNameSkip" app:layout_constraintBottom_toTopOf="@id/displayNameSkip"
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd" app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart" app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
app:layout_constraintTop_toBottomOf="@id/actionsSpacing" /> app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
<Button <Button
android:id="@+id/displayNameSkip" android:id="@+id/displayNameSkip"

View file

@ -122,13 +122,13 @@
android:gravity="center" android:gravity="center"
android:text="@string/ftue_profile_picture_subtitle" android:text="@string/ftue_profile_picture_subtitle"
android:textColor="?vctr_content_secondary" android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/actionsSpacing" app:layout_constraintBottom_toTopOf="@id/entrySpacing"
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd" app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart" app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
app:layout_constraintTop_toBottomOf="@id/profilePictureHeaderTitle" /> app:layout_constraintTop_toBottomOf="@id/profilePictureHeaderTitle" />
<Space <Space
android:id="@+id/actionsSpacing" android:id="@+id/entrySpacing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/profilePictureSubmit" app:layout_constraintBottom_toTopOf="@id/profilePictureSubmit"
@ -145,7 +145,7 @@
app:layout_constraintBottom_toTopOf="@id/profilePictureSkip" app:layout_constraintBottom_toTopOf="@id/profilePictureSkip"
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd" app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart" app:layout_constraintStart_toStartOf="@id/profilePictureGutterStart"
app:layout_constraintTop_toBottomOf="@id/actionsSpacing" /> app:layout_constraintTop_toBottomOf="@id/entrySpacing" />
<Button <Button
android:id="@+id/profilePictureSkip" android:id="@+id/profilePictureSkip"

View file

@ -0,0 +1,293 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
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:id="@+id/createAccountRoot"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/createAccountGutterStart"
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/createAccountGutterEnd"
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/createAccountHeaderIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/createAccountHeaderIcon"
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_user_fg"
app:layout_constraintBottom_toTopOf="@id/createAccountHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing"
app:tint="@color/palette_white" />
<TextView
android:id="@+id/createAccountHeaderTitle"
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_create_account_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/createAccountHeaderSubtitle"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderIcon" />
<TextView
android:id="@+id/createAccountHeaderSubtitle"
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_create_account_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderTitle" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/chooseYourServerHeader"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderSubtitle" />
<TextView
android:id="@+id/chooseYourServerHeader"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:text="@string/ftue_auth_create_account_choose_server_header"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/selectedServerName"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing" />
<TextView
android:id="@+id/selectedServerName"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/ftue_auth_create_account_matrix_dot_org_server_name"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader" />
<TextView
android:id="@+id/selectedServerDescription"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/ftue_auth_create_account_matrix_dot_org_server_description"
android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/selectedServerName" />
<Button
android:id="@+id/editServerButton"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/ftue_auth_create_account_edit_server_selection"
android:textAllCaps="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/selectedServerDescription"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintTop_toTopOf="@id/chooseYourServerHeader" />
<Space
android:id="@+id/serverSelectionSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/createAccountInput"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/selectedServerDescription" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_content_quaternary"
app:layout_constraintBottom_toBottomOf="@id/serverSelectionSpacing"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toTopOf="@id/serverSelectionSpacing" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/createAccountInput"
style="@style/Widget.Vector.TextInputLayout.Username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/username"
app:layout_constraintBottom_toTopOf="@id/createAccountEntryFooter"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/serverSelectionSpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionNext"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/createAccountEntryFooter"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ftue_auth_create_account_username_entry_footer"
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/createAccountInput" />
<Space
android:id="@+id/entrySpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/createAccountPasswordInput"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/createAccountEntryFooter" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/createAccountPasswordInput"
style="@style/Widget.Vector.TextInputLayout.Password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:layout_constraintBottom_toTopOf="@id/createAccountPasswordEntryFooter"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/createAccountPasswordEntryFooter"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/ftue_auth_create_account_password_entry_footer"
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/createAccountPasswordInput" />
<Space
android:id="@+id/actionSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/createAccountSubmit"
app:layout_constraintHeight_percent="0.02"
app:layout_constraintTop_toBottomOf="@id/createAccountPasswordEntryFooter" />
<Button
android:id="@+id/createAccountSubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_signup_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toTopOf="@id/ssoButtonsHeader"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/actionSpacing" />
<androidx.constraintlayout.widget.Group
android:id="@+id/ssoGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="ssoButtonsHeader,ssoButtons"
app:layout_constraintBottom_toTopOf="@id/ssoButtonsHeader"
app:layout_constraintTop_toBottomOf="@id/createAccountSubmit"
tools:visibility="visible" />
<TextView
android:id="@+id/ssoButtonsHeader"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/ftue_auth_create_account_sso_section_header"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/ssoButtons"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/createAccountSubmit" />
<im.vector.app.features.login.SocialLoginButtonsView
android:id="@+id/ssoButtons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
tools:signMode="signup" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -10,4 +10,14 @@
<string name="cut_the_slack_from_teams" translatable="false">Cut the slack from teams.</string> <string name="cut_the_slack_from_teams" translatable="false">Cut the slack from teams.</string>
<!-- WIP --> <!-- WIP -->
<string name="ftue_auth_create_account_title">Create your account</string>
<string name="ftue_auth_create_account_subtitle">We\'ll need some info to get you set up.</string>
<string name="ftue_auth_create_account_username_entry_footer">You can\'t change this later</string>
<string name="ftue_auth_create_account_password_entry_footer">Must be 8 characters or more</string>
<string name="ftue_auth_create_account_choose_server_header">Choose your server to store your data</string>
<string name="ftue_auth_create_account_sso_section_header">Or</string>
<string name="ftue_auth_create_account_matrix_dot_org_server_description">Join millions for free on the largest public server</string>
<string name="ftue_auth_create_account_matrix_dot_org_server_name">matrix.org</string>
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
</resources> </resources>

View file

@ -2470,6 +2470,7 @@
<string name="error_empty_field_choose_user_name">Please choose a username.</string> <string name="error_empty_field_choose_user_name">Please choose a username.</string>
<string name="error_empty_field_choose_password">Please choose a password.</string> <string name="error_empty_field_choose_password">Please choose a password.</string>
<string name="error_forbidden_digits_only_username">"The homeserver does not accept username with only digits."</string>
<string name="external_link_confirmation_title">Double-check this link</string> <string name="external_link_confirmation_title">Double-check this link</string>
<string name="external_link_confirmation_message">The link %1$s is taking you to another site: %2$s.\n\nAre you sure you want to continue?</string> <string name="external_link_confirmation_message">The link %1$s is taking you to another site: %2$s.\n\nAre you sure you want to continue?</string>