mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Submit the exact account provider field text on continue
This commit is contained in:
+4
-1
@@ -10,6 +10,9 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||||||
|
|
||||||
sealed interface ConfirmAccountProviderEvents {
|
sealed interface ConfirmAccountProviderEvents {
|
||||||
data class UserInputChanged(val accountProvider: String) : ConfirmAccountProviderEvents
|
data class UserInputChanged(val accountProvider: String) : ConfirmAccountProviderEvents
|
||||||
data object Continue : ConfirmAccountProviderEvents
|
|
||||||
|
// Carries the account provider to submit (the current field text, or the accepted autocomplete suggestion),
|
||||||
|
// captured from the view at click time so it can't lag behind fast input (e.g. keyboard autofill).
|
||||||
|
data class Continue(val accountProvider: String) : ConfirmAccountProviderEvents
|
||||||
data object ClearError : ConfirmAccountProviderEvents
|
data object ClearError : ConfirmAccountProviderEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -67,9 +67,10 @@ class ConfirmAccountProviderPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue accepts the suggestion when one is offered, otherwise the raw input.
|
// The account provider submitted via Continue (carried by the event, so it reflects the exact field
|
||||||
val accountProviderToSubmit = accountProviderSuggestion ?: accountProviderInput
|
// text at click time); used to actually sign in once validation succeeds.
|
||||||
val latestAccountProviderToSubmit by rememberUpdatedState(accountProviderToSubmit)
|
var submittedAccountProvider by remember { mutableStateOf<String?>(null) }
|
||||||
|
val latestSubmittedAccountProvider by rememberUpdatedState(submittedAccountProvider)
|
||||||
|
|
||||||
// Once the chosen account provider has been validated and persisted, proceed with the actual sign in / sign up.
|
// Once the chosen account provider has been validated and persisted, proceed with the actual sign in / sign up.
|
||||||
LaunchedEffect(changeServerState.changeServerAction) {
|
LaunchedEffect(changeServerState.changeServerAction) {
|
||||||
@@ -77,7 +78,7 @@ class ConfirmAccountProviderPresenter(
|
|||||||
loginModeState.eventSink(
|
loginModeState.eventSink(
|
||||||
LoginModeEvent.Submit(
|
LoginModeEvent.Submit(
|
||||||
isAccountCreation = params.isAccountCreation,
|
isAccountCreation = params.isAccountCreation,
|
||||||
homeserverUrl = latestAccountProviderToSubmit.trim(),
|
homeserverUrl = latestSubmittedAccountProvider.orEmpty().trim(),
|
||||||
resolvedHomeserverUrl = null,
|
resolvedHomeserverUrl = null,
|
||||||
loginHint = null,
|
loginHint = null,
|
||||||
)
|
)
|
||||||
@@ -92,9 +93,12 @@ class ConfirmAccountProviderPresenter(
|
|||||||
}
|
}
|
||||||
// Validate (and persist) the chosen account provider before proceeding. This also enforces the
|
// Validate (and persist) the chosen account provider before proceeding. This also enforces the
|
||||||
// account-provider access control, which the login submit does not run on its own.
|
// account-provider access control, which the login submit does not run on its own.
|
||||||
ConfirmAccountProviderEvents.Continue -> changeServerState.eventSink(
|
is ConfirmAccountProviderEvents.Continue -> {
|
||||||
ChangeServerEvents.ChangeServer(AccountProvider(url = accountProviderToSubmit.trim()))
|
submittedAccountProvider = event.accountProvider
|
||||||
)
|
changeServerState.eventSink(
|
||||||
|
ChangeServerEvents.ChangeServer(AccountProvider(url = event.accountProvider.trim()))
|
||||||
|
)
|
||||||
|
}
|
||||||
ConfirmAccountProviderEvents.ClearError -> {
|
ConfirmAccountProviderEvents.ClearError -> {
|
||||||
loginModeState.eventSink(LoginModeEvent.ClearError)
|
loginModeState.eventSink(LoginModeEvent.ClearError)
|
||||||
changeServerState.eventSink(ChangeServerEvents.ClearError)
|
changeServerState.eventSink(ChangeServerEvents.ClearError)
|
||||||
|
|||||||
+11
-7
@@ -81,10 +81,20 @@ fun ConfirmAccountProviderView(
|
|||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
var input by textFieldState(stateValue = state.accountProviderInput)
|
||||||
|
// Inline autocomplete: the un-typed remainder of the suggested account provider (rendered in grey and
|
||||||
|
// accepted on Continue). Only valid while the suggestion still starts with the current input.
|
||||||
|
val suggestionSuffix = state.accountProviderSuggestion
|
||||||
|
?.takeIf { it.length > input.length && it.startsWith(input, ignoreCase = true) }
|
||||||
|
?.substring(input.length)
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
fun submit() {
|
fun submit() {
|
||||||
// Dismiss the keyboard and release focus while the account provider is being validated.
|
// Dismiss the keyboard and release focus while the account provider is being validated.
|
||||||
focusManager.clearFocus(force = true)
|
focusManager.clearFocus(force = true)
|
||||||
eventSink(ConfirmAccountProviderEvents.Continue)
|
// Submit the exact field text (plus any accepted suggestion), read here so it can't lag behind
|
||||||
|
// fast input such as keyboard autofill.
|
||||||
|
eventSink(ConfirmAccountProviderEvents.Continue(input + suggestionSuffix))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once a validation / login error has been dismissed, return focus and the keyboard to the field so
|
// Once a validation / login error has been dismissed, return focus and the keyboard to the field so
|
||||||
@@ -128,12 +138,6 @@ fun ConfirmAccountProviderView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
var input by textFieldState(stateValue = state.accountProviderInput)
|
|
||||||
// Inline autocomplete: render the un-typed remainder of the suggested account provider in grey.
|
|
||||||
val suggestionSuffix = state.accountProviderSuggestion
|
|
||||||
?.takeIf { it.length > input.length && it.startsWith(input, ignoreCase = true) }
|
|
||||||
?.substring(input.length)
|
|
||||||
.orEmpty()
|
|
||||||
val ghostColor = ElementTheme.colors.textSecondary
|
val ghostColor = ElementTheme.colors.textSecondary
|
||||||
val ghostTransformation = remember(suggestionSuffix, ghostColor) {
|
val ghostTransformation = remember(suggestionSuffix, ghostColor) {
|
||||||
GhostSuffixVisualTransformation(suggestionSuffix, ghostColor)
|
GhostSuffixVisualTransformation(suggestionSuffix, ghostColor)
|
||||||
|
|||||||
+14
-14
@@ -70,7 +70,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
@@ -89,7 +89,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
@@ -110,7 +110,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
||||||
@@ -135,7 +135,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||||
@@ -159,7 +159,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
awaitLoginMode { it is AsyncData.Loading }
|
awaitLoginMode { it is AsyncData.Loading }
|
||||||
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||||
val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized }
|
val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized }
|
||||||
@@ -182,7 +182,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
authenticationService.givenLoginError(AN_EXCEPTION)
|
authenticationService.givenLoginError(AN_EXCEPTION)
|
||||||
@@ -207,7 +207,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val successState = awaitLoginMode { it is AsyncData.Success }
|
val successState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
||||||
@@ -229,7 +229,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
// The account provider validation fails, so the login is never attempted.
|
// The account provider validation fails, so the login is never attempted.
|
||||||
val failureState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
val failureState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
||||||
assertThat(failureState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
assertThat(failureState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized)
|
||||||
@@ -251,7 +251,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
// Submit will return an error while validating the account provider
|
// Submit will return an error while validating the account provider
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
|
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
val submittedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure }
|
||||||
@@ -279,7 +279,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
// Check an error was returned
|
// Check an error was returned
|
||||||
val submittedState = awaitLoginMode { it is AsyncData.Failure }
|
val submittedState = awaitLoginMode { it is AsyncData.Failure }
|
||||||
assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
|
assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java)
|
||||||
@@ -304,7 +304,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
@@ -326,7 +326,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
@@ -348,7 +348,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink(ConfirmAccountProviderEvents.Continue(AuthenticationConfig.MATRIX_ORG_URL))
|
||||||
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
val submittedState = awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
|
assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl))
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
@@ -390,7 +390,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ConfirmAccountProviderEvents.UserInputChanged("https://random"))
|
initialState.eventSink(ConfirmAccountProviderEvents.UserInputChanged("https://random"))
|
||||||
val suggestionState = awaitState { it.accountProviderSuggestion != null }
|
val suggestionState = awaitState { it.accountProviderSuggestion != null }
|
||||||
suggestionState.eventSink(ConfirmAccountProviderEvents.Continue)
|
suggestionState.eventSink(ConfirmAccountProviderEvents.Continue("https://randomcommunity.org"))
|
||||||
awaitLoginMode { it is AsyncData.Success }
|
awaitLoginMode { it is AsyncData.Success }
|
||||||
assertThat(submittedUrls.first()).isEqualTo("https://randomcommunity.org")
|
assertThat(submittedUrls.first()).isEqualTo("https://randomcommunity.org")
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
|||||||
Reference in New Issue
Block a user