Merge remote-tracking branch 'origin/develop' into task/eric/code-style-parenthesis
# Conflicts: # vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
This commit is contained in:
commit
d5432cd0e7
|
@ -0,0 +1 @@
|
||||||
|
FTUE - Adds the redesigned Sign In screen
|
|
@ -60,6 +60,11 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||||
key = DebugFeatureKeys.onboardingCombinedRegister,
|
key = DebugFeatureKeys.onboardingCombinedRegister,
|
||||||
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
|
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
|
||||||
),
|
),
|
||||||
|
createBooleanFeature(
|
||||||
|
label = "FTUE Combined login",
|
||||||
|
key = DebugFeatureKeys.onboardingCombinedLogin,
|
||||||
|
factory = VectorFeatures::isOnboardingCombinedLoginEnabled
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,9 @@ class DebugVectorFeatures(
|
||||||
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
|
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
|
||||||
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
|
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
|
||||||
|
|
||||||
|
override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin)
|
||||||
|
?: vectorFeatures.isOnboardingCombinedLoginEnabled()
|
||||||
|
|
||||||
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
|
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
|
||||||
?: vectorFeatures.isScreenSharingEnabled()
|
?: vectorFeatures.isScreenSharingEnabled()
|
||||||
|
|
||||||
|
@ -113,6 +116,7 @@ object DebugFeatureKeys {
|
||||||
val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
|
val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
|
||||||
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
|
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
|
||||||
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
|
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
|
||||||
|
val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login")
|
||||||
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
||||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,9 @@ 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.FtueAuthCombinedLoginFragment
|
||||||
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedRegisterFragment
|
||||||
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment
|
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
|
||||||
|
@ -521,6 +524,21 @@ interface FragmentModule {
|
||||||
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
|
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
|
||||||
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
|
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(FtueAuthCombinedLoginFragment::class)
|
||||||
|
fun bindFtueAuthCombinedLoginFragment(fragment: FtueAuthCombinedLoginFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(FtueAuthCombinedRegisterFragment::class)
|
||||||
|
fun bindFtueAuthCombinedRegisterFragment(fragment: FtueAuthCombinedRegisterFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(FtueAuthCombinedServerSelectionFragment::class)
|
||||||
|
fun bindFtueAuthCombinedServerSelectionFragment(fragment: FtueAuthCombinedServerSelectionFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(UserListFragment::class)
|
@FragmentKey(UserListFragment::class)
|
||||||
|
|
|
@ -26,6 +26,7 @@ interface VectorFeatures {
|
||||||
fun isOnboardingUseCaseEnabled(): Boolean
|
fun isOnboardingUseCaseEnabled(): Boolean
|
||||||
fun isOnboardingPersonalizeEnabled(): Boolean
|
fun isOnboardingPersonalizeEnabled(): Boolean
|
||||||
fun isOnboardingCombinedRegisterEnabled(): Boolean
|
fun isOnboardingCombinedRegisterEnabled(): Boolean
|
||||||
|
fun isOnboardingCombinedLoginEnabled(): Boolean
|
||||||
fun isScreenSharingEnabled(): Boolean
|
fun isScreenSharingEnabled(): Boolean
|
||||||
|
|
||||||
enum class OnboardingVariant {
|
enum class OnboardingVariant {
|
||||||
|
@ -42,5 +43,6 @@ class DefaultVectorFeatures : VectorFeatures {
|
||||||
override fun isOnboardingUseCaseEnabled() = true
|
override fun isOnboardingUseCaseEnabled() = true
|
||||||
override fun isOnboardingPersonalizeEnabled() = false
|
override fun isOnboardingPersonalizeEnabled() = false
|
||||||
override fun isOnboardingCombinedRegisterEnabled() = false
|
override fun isOnboardingCombinedRegisterEnabled() = false
|
||||||
|
override fun isOnboardingCombinedLoginEnabled() = false
|
||||||
override fun isScreenSharingEnabled(): Boolean = true
|
override fun isScreenSharingEnabled(): Boolean = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,3 +159,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
|
||||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
|
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (String?) -> Unit) {
|
||||||
|
this.mode = mode
|
||||||
|
this.ssoIdentityProviders = ssoProviders?.sorted()
|
||||||
|
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.app.features.onboarding
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.andThen
|
import im.vector.app.core.extensions.andThen
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
|
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction.LoginDirect
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
|
@ -33,8 +33,8 @@ class DirectLoginUseCase @Inject constructor(
|
||||||
private val uriFactory: UriFactory
|
private val uriFactory: UriFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
|
suspend fun execute(action: LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
|
||||||
return fetchWellKnown(action.username, homeServerConnectionConfig)
|
return fetchWellKnown(action.matrixId, homeServerConnectionConfig)
|
||||||
.andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
|
.andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +42,13 @@ class DirectLoginUseCase @Inject constructor(
|
||||||
authenticationService.getWellKnownData(matrixId, config)
|
authenticationService.getWellKnownData(matrixId, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) {
|
private suspend fun createSessionFor(data: WellknownResult, action: LoginDirect, config: HomeServerConnectionConfig?) = when (data) {
|
||||||
is WellknownResult.Prompt -> loginDirect(action, data, config)
|
is WellknownResult.Prompt -> loginDirect(action, data, config)
|
||||||
is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
|
is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
|
||||||
else -> onWellKnownError()
|
else -> onWellKnownError()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result<Session> {
|
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginDirect, config: HomeServerConnectionConfig?): Result<Session> {
|
||||||
// Relax on IS discovery if homeserver is valid
|
// Relax on IS discovery if homeserver is valid
|
||||||
val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
|
val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
|
||||||
return when {
|
return when {
|
||||||
|
@ -57,12 +57,12 @@ class DirectLoginUseCase @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
|
private suspend fun loginDirect(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
|
||||||
val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
|
val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
|
||||||
return runCatching {
|
return runCatching {
|
||||||
authenticationService.directAuthentication(
|
authenticationService.directAuthentication(
|
||||||
alteredHomeServerConnectionConfig,
|
alteredHomeServerConnectionConfig,
|
||||||
action.username,
|
action.matrixId,
|
||||||
action.password,
|
action.password,
|
||||||
action.initialDeviceName
|
action.initialDeviceName
|
||||||
)
|
)
|
||||||
|
@ -74,8 +74,8 @@ class DirectLoginUseCase @Inject constructor(
|
||||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
|
private fun fallbackConfig(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
|
||||||
homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"),
|
homeServerUri = uriFactory.parse("https://${action.matrixId.getServerName()}"),
|
||||||
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
|
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
|
||||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||||
)
|
)
|
||||||
|
|
|
@ -46,9 +46,12 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
||||||
object ResetPasswordMailConfirmed : OnboardingAction
|
object ResetPasswordMailConfirmed : OnboardingAction
|
||||||
|
|
||||||
// Login or Register, depending on the signMode
|
sealed interface AuthenticateAction : 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) : AuthenticateAction
|
||||||
data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
|
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
|
data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
|
}
|
||||||
|
|
||||||
object StopEmailValidationCheck : OnboardingAction
|
object StopEmailValidationCheck : OnboardingAction
|
||||||
|
|
||||||
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
|
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
|
||||||
|
|
|
@ -37,6 +37,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
||||||
object OpenUseCaseSelection : OnboardingViewEvents()
|
object OpenUseCaseSelection : OnboardingViewEvents()
|
||||||
object OpenServerSelection : OnboardingViewEvents()
|
object OpenServerSelection : OnboardingViewEvents()
|
||||||
object OpenCombinedRegister : OnboardingViewEvents()
|
object OpenCombinedRegister : OnboardingViewEvents()
|
||||||
|
object OpenCombinedLogin : OnboardingViewEvents()
|
||||||
object EditServerSelection : OnboardingViewEvents()
|
object EditServerSelection : OnboardingViewEvents()
|
||||||
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
|
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
|
||||||
object OnLoginFlowRetrieved : OnboardingViewEvents()
|
object OnLoginFlowRetrieved : OnboardingViewEvents()
|
||||||
|
|
|
@ -42,6 +42,7 @@ import im.vector.app.features.login.LoginMode
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import im.vector.app.features.login.ServerType
|
import im.vector.app.features.login.ServerType
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||||
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
||||||
import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
|
import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -139,8 +140,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||||
is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
|
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(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)
|
||||||
|
@ -165,6 +165,14 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
block(action)
|
block(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleAuthenticateAction(action: AuthenticateAction) {
|
||||||
|
when (action) {
|
||||||
|
is AuthenticateAction.Register -> handleRegisterWith(action)
|
||||||
|
is AuthenticateAction.Login -> handleLogin(action)
|
||||||
|
is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) {
|
private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) {
|
||||||
if (resetConfig) {
|
if (resetConfig) {
|
||||||
loginConfig = null
|
loginConfig = null
|
||||||
|
@ -188,16 +196,21 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
|
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
|
||||||
val nextOnboardingStep = when (onboardingFlow) {
|
when (onboardingFlow) {
|
||||||
OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
OnboardingFlow.SignUp -> {
|
||||||
OnboardingViewEvents.OpenUseCaseSelection
|
_viewEvents.post(
|
||||||
} else {
|
if (vectorFeatures.isOnboardingUseCaseEnabled()) {
|
||||||
OnboardingViewEvents.OpenServerSelection
|
OnboardingViewEvents.OpenUseCaseSelection
|
||||||
|
} else {
|
||||||
|
OnboardingViewEvents.OpenServerSelection
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
OnboardingFlow.SignIn,
|
OnboardingFlow.SignIn -> if (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
|
||||||
OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection
|
handle(OnboardingAction.HomeServerChange.SelectHomeServer(defaultHomeserverUrl))
|
||||||
|
} else _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||||
|
OnboardingFlow.SignInSignUp -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||||
}
|
}
|
||||||
_viewEvents.post(nextOnboardingStep)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
|
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
|
||||||
|
@ -209,7 +222,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||||
?.let { startAuthenticationFlow(finalLastAction, it) }
|
?.let { startAuthenticationFlow(finalLastAction, it) }
|
||||||
}
|
}
|
||||||
is OnboardingAction.LoginOrRegister ->
|
is AuthenticateAction.LoginDirect ->
|
||||||
handleDirectLogin(
|
handleDirectLogin(
|
||||||
finalLastAction,
|
finalLastAction,
|
||||||
HomeServerConnectionConfig.Builder()
|
HomeServerConnectionConfig.Builder()
|
||||||
|
@ -307,7 +320,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
|
private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
|
||||||
|
|
||||||
private fun handleRegisterWith(action: OnboardingAction.Register) {
|
private fun handleRegisterWith(action: AuthenticateAction.Register) {
|
||||||
reAuthHelper.data = action.password
|
reAuthHelper.data = action.password
|
||||||
handleRegisterAction(
|
handleRegisterAction(
|
||||||
RegisterAction.CreateAccount(
|
RegisterAction.CreateAccount(
|
||||||
|
@ -482,16 +495,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoginOrRegister(action: OnboardingAction.LoginOrRegister) = withState { state ->
|
private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
||||||
when (state.signMode) {
|
|
||||||
SignMode.Unknown -> error("Developer error, invalid sign mode")
|
|
||||||
SignMode.SignIn -> handleLogin(action)
|
|
||||||
SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName))
|
|
||||||
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
|
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
|
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
|
||||||
|
@ -504,7 +508,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLogin(action: OnboardingAction.LoginOrRegister) {
|
private fun handleLogin(action: AuthenticateAction.Login) {
|
||||||
val safeLoginWizard = loginWizard
|
val safeLoginWizard = loginWizard
|
||||||
|
|
||||||
if (safeLoginWizard == null) {
|
if (safeLoginWizard == null) {
|
||||||
|
@ -648,7 +652,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
when (trigger) {
|
when (trigger) {
|
||||||
is OnboardingAction.HomeServerChange.EditHomeServer -> {
|
is OnboardingAction.HomeServerChange.EditHomeServer -> {
|
||||||
when (awaitState().onboardingFlow) {
|
when (awaitState().onboardingFlow) {
|
||||||
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { _ ->
|
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) {
|
||||||
|
updateServerSelection(config, serverTypeOverride, authResult)
|
||||||
|
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||||
|
}
|
||||||
|
OnboardingFlow.SignIn -> {
|
||||||
updateServerSelection(config, serverTypeOverride, authResult)
|
updateServerSelection(config, serverTypeOverride, authResult)
|
||||||
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
|
||||||
}
|
}
|
||||||
|
@ -661,7 +669,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
when (awaitState().onboardingFlow) {
|
when (awaitState().onboardingFlow) {
|
||||||
OnboardingFlow.SignIn -> {
|
OnboardingFlow.SignIn -> {
|
||||||
updateSignMode(SignMode.SignIn)
|
updateSignMode(SignMode.SignIn)
|
||||||
_viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
when (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
|
||||||
|
true -> _viewEvents.post(OnboardingViewEvents.OpenCombinedLogin)
|
||||||
|
false -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OnboardingFlow.SignUp -> {
|
OnboardingFlow.SignUp -> {
|
||||||
updateSignMode(SignMode.SignUp)
|
updateSignMode(SignMode.SignUp)
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.autofill.HintConstants
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
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.hideKeyboard
|
||||||
|
import im.vector.app.core.extensions.hidePassword
|
||||||
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
|
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
||||||
|
import im.vector.app.features.login.LoginMode
|
||||||
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
|
import im.vector.app.features.login.SocialLoginButtonsView
|
||||||
|
import im.vector.app.features.login.render
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||||
|
private val loginFieldsValidation: LoginFieldsValidation,
|
||||||
|
private val loginErrorParser: LoginErrorParser
|
||||||
|
) : AbstractSSOFtueAuthFragment<FragmentFtueCombinedLoginBinding>() {
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedLoginBinding {
|
||||||
|
return FragmentFtueCombinedLoginBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupSubmitButton()
|
||||||
|
views.loginRoot.realignPercentagesToParent()
|
||||||
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
|
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSubmitButton() {
|
||||||
|
views.loginSubmit.setOnClickListener { submit() }
|
||||||
|
observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit)
|
||||||
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submit() {
|
||||||
|
cleanupUi()
|
||||||
|
loginFieldsValidation.validate(views.loginInput.content(), views.loginPasswordInput.content())
|
||||||
|
.onUsernameOrIdError { views.loginInput.error = it }
|
||||||
|
.onPasswordError { views.loginPasswordInput.error = it }
|
||||||
|
.onValid { usernameOrId, password ->
|
||||||
|
val initialDeviceName = getString(R.string.login_default_session_public_name)
|
||||||
|
viewModel.handle(OnboardingAction.AuthenticateAction.Login(usernameOrId, password, initialDeviceName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupUi() {
|
||||||
|
views.loginSubmit.hideKeyboard()
|
||||||
|
views.loginInput.error = null
|
||||||
|
views.loginPasswordInput.error = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetViewModel() {
|
||||||
|
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(throwable: Throwable) {
|
||||||
|
// Trick to display the error without text.
|
||||||
|
views.loginInput.error = " "
|
||||||
|
loginErrorParser.parse(throwable, views.loginPasswordInput.content())
|
||||||
|
.onUnknown { super.onError(it) }
|
||||||
|
.onUsernameOrIdError { views.loginInput.error = it }
|
||||||
|
.onPasswordError { views.loginPasswordInput.error = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
setupUi(state)
|
||||||
|
setupAutoFill()
|
||||||
|
|
||||||
|
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||||
|
views.selectedServerDescription.text = state.selectedHomeserver.description
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
// Ensure password is hidden
|
||||||
|
views.loginPasswordInput.editText().hidePassword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUi(state: OnboardingViewState) {
|
||||||
|
when (state.selectedHomeserver.preferredLoginMode) {
|
||||||
|
is LoginMode.SsoAndPassword -> {
|
||||||
|
showUsernamePassword()
|
||||||
|
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||||
|
}
|
||||||
|
is LoginMode.Sso -> {
|
||||||
|
hideUsernamePassword()
|
||||||
|
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
showUsernamePassword()
|
||||||
|
hideSsoProviders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||||
|
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||||
|
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
|
||||||
|
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { 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 hideUsernamePassword() {
|
||||||
|
views.loginEntryGroup.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUsernamePassword() {
|
||||||
|
views.loginEntryGroup.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAutoFill() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
|
||||||
|
views.loginPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import androidx.autofill.HintConstants
|
import androidx.autofill.HintConstants
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -31,22 +30,22 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
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.hasSurroundingSpaces
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.hidePassword
|
import im.vector.app.core.extensions.hidePassword
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||||
import im.vector.app.features.login.SocialLoginButtonsView
|
import im.vector.app.features.login.SocialLoginButtonsView
|
||||||
|
import im.vector.app.features.login.render
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
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.auth.data.SsoIdentityProvider
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||||
|
@ -66,36 +65,16 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupSubmitButton()
|
setupSubmitButton()
|
||||||
views.createAccountRoot.realignPercentagesToParent()
|
views.createAccountRoot.realignPercentagesToParent()
|
||||||
views.editServerButton.debouncedClicks {
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection))
|
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||||
}
|
|
||||||
|
|
||||||
views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
submit()
|
|
||||||
return@setOnEditorActionListener true
|
|
||||||
}
|
|
||||||
return@setOnEditorActionListener false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
views.createAccountSubmit.setOnClickListener { submit() }
|
views.createAccountSubmit.setOnClickListener { submit() }
|
||||||
observeInputFields()
|
observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit)
|
||||||
.onEach {
|
|
||||||
views.createAccountPasswordInput.error = null
|
|
||||||
views.createAccountInput.error = null
|
|
||||||
views.createAccountSubmit.isEnabled = it
|
|
||||||
}
|
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeInputFields() = combine(
|
|
||||||
views.createAccountInput.hasContentFlow { it.trim() },
|
|
||||||
views.createAccountPasswordInput.hasContentFlow(),
|
|
||||||
transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty }
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun submit() {
|
private fun submit() {
|
||||||
withState(viewModel) { state ->
|
withState(viewModel) { state ->
|
||||||
cleanupUi()
|
cleanupUi()
|
||||||
|
@ -119,7 +98,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error == 0) {
|
if (error == 0) {
|
||||||
viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name)))
|
viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,9 +164,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||||
|
|
||||||
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||||
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||||
views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE
|
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
|
||||||
views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted()
|
|
||||||
views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id ->
|
|
||||||
viewModel.getSsoUrl(
|
viewModel.getSsoUrl(
|
||||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.autofill.HintConstants
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
|
@ -119,40 +120,43 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submit() {
|
private fun submit() {
|
||||||
cleanupUi()
|
withState(viewModel) { state ->
|
||||||
|
cleanupUi()
|
||||||
|
|
||||||
val login = views.loginField.text.toString()
|
val login = views.loginField.text.toString()
|
||||||
val password = views.passwordField.text.toString()
|
val password = views.passwordField.text.toString()
|
||||||
|
|
||||||
// This can be called by the IME action, so deal with empty cases
|
// This can be called by the IME action, so deal with empty cases
|
||||||
var error = 0
|
var error = 0
|
||||||
if (login.isEmpty()) {
|
if (login.isEmpty()) {
|
||||||
views.loginFieldTil.error = getString(
|
views.loginFieldTil.error = getString(
|
||||||
if (isSignupMode) {
|
if (isSignupMode) {
|
||||||
R.string.error_empty_field_choose_user_name
|
R.string.error_empty_field_choose_user_name
|
||||||
} else {
|
} else {
|
||||||
R.string.error_empty_field_enter_user_name
|
R.string.error_empty_field_enter_user_name
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
error++
|
error++
|
||||||
}
|
}
|
||||||
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
|
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
|
||||||
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
|
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
|
||||||
error++
|
error++
|
||||||
}
|
}
|
||||||
if (password.isEmpty()) {
|
if (password.isEmpty()) {
|
||||||
views.passwordFieldTil.error = getString(
|
views.passwordFieldTil.error = getString(
|
||||||
if (isSignupMode) {
|
if (isSignupMode) {
|
||||||
R.string.error_empty_field_choose_password
|
R.string.error_empty_field_choose_password
|
||||||
} else {
|
} else {
|
||||||
R.string.error_empty_field_your_password
|
R.string.error_empty_field_your_password
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
error++
|
error++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error == 0) {
|
if (error == 0) {
|
||||||
viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name)))
|
val initialDeviceName = getString(R.string.login_default_session_public_name)
|
||||||
|
viewModel.handle(state.signMode.toAuthenticateAction(login, password, initialDeviceName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,10 +227,15 @@ class FtueAuthVariant(
|
||||||
option = commonOption
|
option = commonOption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
|
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
|
||||||
|
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onStartCombinedLogin() {
|
||||||
|
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
|
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
|
||||||
when {
|
when {
|
||||||
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()
|
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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.widget.Button
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import im.vector.app.core.extensions.hasContentFlow
|
||||||
|
import im.vector.app.features.login.SignMode
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
|
||||||
|
return when (this) {
|
||||||
|
SignMode.Unknown -> error("developer error")
|
||||||
|
SignMode.SignUp -> OnboardingAction.AuthenticateAction.Register(username = login, password, initialDeviceName)
|
||||||
|
SignMode.SignIn -> OnboardingAction.AuthenticateAction.Login(username = login, password, initialDeviceName)
|
||||||
|
SignMode.SignInWithMatrixId -> OnboardingAction.AuthenticateAction.LoginDirect(matrixId = login, password, initialDeviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flow to monitor content changes from both username/id and password fields,
|
||||||
|
* clearing errors and enabling/disabling the submission button on non empty content changes.
|
||||||
|
*/
|
||||||
|
fun observeContentChangesAndResetErrors(username: TextInputLayout, password: TextInputLayout, submit: Button): Flow<*> {
|
||||||
|
return combine(
|
||||||
|
username.hasContentFlow { it.trim() },
|
||||||
|
password.hasContentFlow(),
|
||||||
|
transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent }
|
||||||
|
).onEach {
|
||||||
|
username.error = null
|
||||||
|
password.error = null
|
||||||
|
submit.isEnabled = it
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.R
|
||||||
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult
|
||||||
|
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 javax.inject.Inject
|
||||||
|
|
||||||
|
class LoginErrorParser @Inject constructor(
|
||||||
|
private val errorFormatter: ErrorFormatter,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
) {
|
||||||
|
fun parse(throwable: Throwable, password: String): LoginErrorResult {
|
||||||
|
return when {
|
||||||
|
throwable.isInvalidUsername() -> {
|
||||||
|
LoginErrorResult(throwable, usernameOrIdError = errorFormatter.toHumanReadable(throwable))
|
||||||
|
}
|
||||||
|
throwable.isLoginEmailUnknown() -> {
|
||||||
|
LoginErrorResult(throwable, usernameOrIdError = stringProvider.getString(R.string.login_login_with_email_error))
|
||||||
|
}
|
||||||
|
throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> {
|
||||||
|
LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LoginErrorResult(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.hasSurroundingSpaces() = trim() != this
|
||||||
|
|
||||||
|
data class LoginErrorResult(val cause: Throwable, val usernameOrIdError: String? = null, val passwordError: String? = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginErrorResult.onUnknown(action: (Throwable) -> Unit): LoginErrorResult {
|
||||||
|
when {
|
||||||
|
usernameOrIdError == null && passwordError == null -> action(cause)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginErrorResult.onUsernameOrIdError(action: (String) -> Unit): LoginErrorResult {
|
||||||
|
usernameOrIdError?.let(action)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginErrorResult.onPasswordError(action: (String) -> Unit): LoginErrorResult {
|
||||||
|
passwordError?.let(action)
|
||||||
|
return this
|
||||||
|
}
|
|
@ -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 im.vector.app.R
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LoginFieldsValidation @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun validate(usernameOrId: String, password: String): LoginValidationResult {
|
||||||
|
return LoginValidationResult(usernameOrId, password, validateUsernameOrId(usernameOrId), validatePassword(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateUsernameOrId(usernameOrId: String): String? {
|
||||||
|
val accountError = when {
|
||||||
|
usernameOrId.isEmpty() -> stringProvider.getString(R.string.error_empty_field_enter_user_name)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return accountError
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validatePassword(password: String): String? {
|
||||||
|
val passwordError = when {
|
||||||
|
password.isEmpty() -> stringProvider.getString(R.string.error_empty_field_your_password)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return passwordError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginValidationResult.onValid(action: (String, String) -> Unit): LoginValidationResult {
|
||||||
|
when {
|
||||||
|
usernameOrIdError == null && passwordError == null -> action(usernameOrId, password)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginValidationResult.onUsernameOrIdError(action: (String) -> Unit): LoginValidationResult {
|
||||||
|
usernameOrIdError?.let(action)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoginValidationResult.onPasswordError(action: (String) -> Unit): LoginValidationResult {
|
||||||
|
passwordError?.let(action)
|
||||||
|
return this
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
data class LoginValidationResult(
|
||||||
|
val usernameOrId: String,
|
||||||
|
val password: String,
|
||||||
|
val usernameOrIdError: String?,
|
||||||
|
val passwordError: String?
|
||||||
|
)
|
|
@ -0,0 +1,244 @@
|
||||||
|
<?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/loginRoot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/loginGutterStart"
|
||||||
|
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/loginGutterEnd"
|
||||||
|
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/loginHeaderTitle"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/loginHeaderTitle"
|
||||||
|
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_welcome_back_title"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headerSpacing" />
|
||||||
|
|
||||||
|
<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/loginHeaderTitle" />
|
||||||
|
|
||||||
|
<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/loginGutterStart"
|
||||||
|
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:textColor="?vctr_content_primary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/editServerButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
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:textColor="?vctr_content_tertiary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/editServerButton"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
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"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/selectedServerDescription"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
|
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/loginInput"
|
||||||
|
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/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/serverSelectionSpacing" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/loginEntryGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:constraint_referenced_ids="loginInput,loginPasswordInput,entrySpacing,actionSpacing,loginSubmit" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/loginInput"
|
||||||
|
style="@style/Widget.Vector.TextInputLayout.Username"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/username"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
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"
|
||||||
|
android:nextFocusForward="@id/loginPasswordInput" />
|
||||||
|
|
||||||
|
</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/loginPasswordInput"
|
||||||
|
app:layout_constraintHeight_percent="0.03"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginInput" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/loginPasswordInput"
|
||||||
|
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/actionSpacing"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/actionSpacing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/loginSubmit"
|
||||||
|
app:layout_constraintHeight_percent="0.02"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/loginSubmit"
|
||||||
|
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/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
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/loginSubmit"
|
||||||
|
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/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/loginSubmit" />
|
||||||
|
|
||||||
|
<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/loginGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
|
||||||
|
tools:signMode="signup" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -19,6 +19,8 @@
|
||||||
<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_description">Join millions for free on the largest public server</string>
|
||||||
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
|
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_welcome_back_title">Welcome back!</string>
|
||||||
|
|
||||||
<string name="ftue_auth_choose_server_title">Choose your server</string>
|
<string name="ftue_auth_choose_server_title">Choose your server</string>
|
||||||
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? Server is like a home for all your data.</string>
|
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? Server is like a home for all your data.</string>
|
||||||
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>
|
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>
|
||||||
|
|
|
@ -32,13 +32,13 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.WellKnown
|
import org.matrix.android.sdk.api.auth.data.WellKnown
|
||||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||||
|
|
||||||
private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
|
private val A_DIRECT_LOGIN_ACTION = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
|
||||||
private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown())
|
private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown())
|
||||||
private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown())
|
private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown())
|
||||||
private val A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT = WellknownResult.FailPrompt(null, null)
|
private val A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT = WellknownResult.FailPrompt(null, null)
|
||||||
private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null
|
private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null
|
||||||
private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig(
|
private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig(
|
||||||
homeServerUri = FakeUri("https://${A_LOGIN_OR_REGISTER_ACTION.username.getServerName()}").instance,
|
homeServerUri = FakeUri("https://${A_DIRECT_LOGIN_ACTION.matrixId.getServerName()}").instance,
|
||||||
homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance,
|
homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance,
|
||||||
identityServerUri = null
|
identityServerUri = null
|
||||||
)
|
)
|
||||||
|
@ -54,11 +54,11 @@ class DirectLoginUseCaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when logging in directly, then returns success with direct session result`() = runTest {
|
fun `when logging in directly, then returns success with direct session result`() = runTest {
|
||||||
fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
|
fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
|
||||||
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
|
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
|
||||||
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
||||||
|
|
||||||
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
||||||
|
|
||||||
result shouldBeEqualTo Result.success(fakeSession)
|
result shouldBeEqualTo Result.success(fakeSession)
|
||||||
}
|
}
|
||||||
|
@ -66,14 +66,14 @@ class DirectLoginUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest {
|
fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest {
|
||||||
fakeAuthenticationService.givenWellKnown(
|
fakeAuthenticationService.givenWellKnown(
|
||||||
A_LOGIN_OR_REGISTER_ACTION.username,
|
A_DIRECT_LOGIN_ACTION.matrixId,
|
||||||
config = NO_HOMESERVER_CONFIG,
|
config = NO_HOMESERVER_CONFIG,
|
||||||
result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT
|
result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT
|
||||||
)
|
)
|
||||||
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
|
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
|
||||||
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
||||||
|
|
||||||
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
||||||
|
|
||||||
result shouldBeEqualTo Result.success(fakeSession)
|
result shouldBeEqualTo Result.success(fakeSession)
|
||||||
}
|
}
|
||||||
|
@ -81,14 +81,14 @@ class DirectLoginUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest {
|
fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest {
|
||||||
fakeAuthenticationService.givenWellKnown(
|
fakeAuthenticationService.givenWellKnown(
|
||||||
A_LOGIN_OR_REGISTER_ACTION.username,
|
A_DIRECT_LOGIN_ACTION.matrixId,
|
||||||
config = NO_HOMESERVER_CONFIG,
|
config = NO_HOMESERVER_CONFIG,
|
||||||
result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT
|
result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT
|
||||||
)
|
)
|
||||||
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
|
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
|
||||||
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
|
||||||
|
|
||||||
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
||||||
|
|
||||||
result should { this.isFailure }
|
result should { this.isFailure }
|
||||||
result should { this.exceptionOrNull() is Exception }
|
result should { this.exceptionOrNull() is Exception }
|
||||||
|
@ -97,20 +97,20 @@ class DirectLoginUseCaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given wellknown throws, when logging in directly, then returns failure result with original cause`() = runTest {
|
fun `given wellknown throws, when logging in directly, then returns failure result with original cause`() = runTest {
|
||||||
fakeAuthenticationService.givenWellKnownThrows(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
|
fakeAuthenticationService.givenWellKnownThrows(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
|
||||||
|
|
||||||
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
||||||
|
|
||||||
result shouldBeEqualTo Result.failure(AN_ERROR)
|
result shouldBeEqualTo Result.failure(AN_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given direct authentication throws, when logging in directly, then returns failure result with original cause`() = runTest {
|
fun `given direct authentication throws, when logging in directly, then returns failure result with original cause`() = runTest {
|
||||||
fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
|
fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
|
||||||
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
|
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
|
||||||
fakeAuthenticationService.givenDirectAuthenticationThrows(A_FALLBACK_CONFIG, username, password, initialDeviceName, cause = AN_ERROR)
|
fakeAuthenticationService.givenDirectAuthenticationThrows(A_FALLBACK_CONFIG, username, password, initialDeviceName, cause = AN_ERROR)
|
||||||
|
|
||||||
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
|
||||||
|
|
||||||
result shouldBeEqualTo Result.failure(AN_ERROR)
|
result shouldBeEqualTo Result.failure(AN_ERROR)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
|
||||||
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.NextStep(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_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@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)
|
||||||
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
|
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
|
||||||
|
@ -142,11 +142,11 @@ class OnboardingViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
|
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
|
||||||
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||||
fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
|
fakeDirectLoginUseCase.givenSuccessResult(A_DIRECT_LOGIN, config = null, result = fakeSession)
|
||||||
givenInitialisesSession(fakeSession)
|
givenInitialisesSession(fakeSession)
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
|
viewModel.handle(A_DIRECT_LOGIN)
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStatesChanges(
|
.assertStatesChanges(
|
||||||
|
@ -161,11 +161,11 @@ class OnboardingViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
|
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
|
||||||
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||||
fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
|
fakeDirectLoginUseCase.givenFailureResult(A_DIRECT_LOGIN, config = null, cause = AN_ERROR)
|
||||||
givenInitialisesSession(fakeSession)
|
givenInitialisesSession(fakeSession)
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
|
viewModel.handle(A_DIRECT_LOGIN)
|
||||||
|
|
||||||
test
|
test
|
||||||
.assertStatesChanges(
|
.assertStatesChanges(
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
import im.vector.app.features.onboarding.DirectLoginUseCase
|
import im.vector.app.features.onboarding.DirectLoginUseCase
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
|
@ -25,11 +25,11 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
class FakeDirectLoginUseCase {
|
class FakeDirectLoginUseCase {
|
||||||
val instance = mockk<DirectLoginUseCase>()
|
val instance = mockk<DirectLoginUseCase>()
|
||||||
|
|
||||||
fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) {
|
fun givenSuccessResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, result: FakeSession) {
|
||||||
coEvery { instance.execute(action, config) } returns Result.success(result)
|
coEvery { instance.execute(action, config) } returns Result.success(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenFailureResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, cause: Throwable) {
|
fun givenFailureResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, cause: Throwable) {
|
||||||
coEvery { instance.execute(action, config) } returns Result.failure(cause)
|
coEvery { instance.execute(action, config) } returns Result.failure(cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue