diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index fbadcddc15..adb02b0f3d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.changeserver.ChangeServerEvents @@ -28,6 +29,7 @@ import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.preferences.api.store.AppPreferencesStore @AssistedInject @@ -58,12 +60,18 @@ class ConfirmAccountProviderPresenter( var userInput by rememberSaveable { mutableStateOf(null) } val accountProviderInput = userInput ?: accountProvider.url - // Offer the most recent previously-used account provider that the current input is a prefix of. + // Offer an account provider that the current input is a prefix of: the most recent previously-used + // one first, then matrix.org. matrix.org is always available for autocomplete even before any + // sign-in, offered both with and without the https:// scheme so it completes whether the user starts + // typing "matrix.org" or "https://matrix.org". val accountProviderSuggestion = remember(accountProviderInput, homeserverHistory) { val input = accountProviderInput.trim() + val candidates = homeserverHistory + + AuthenticationConfig.MATRIX_ORG_URL + + AuthenticationConfig.MATRIX_ORG_URL.removePrefix("https://") input.takeIf { it.isNotEmpty() } ?.let { prefix -> - homeserverHistory.firstOrNull { it.length > prefix.length && it.startsWith(prefix, ignoreCase = true) } + candidates.firstOrNull { it.length > prefix.length && it.startsWith(prefix, ignoreCase = true) } } } @@ -94,9 +102,11 @@ class ConfirmAccountProviderPresenter( // 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. is ConfirmAccountProviderEvents.Continue -> { - submittedAccountProvider = event.accountProvider + // Default the scheme to https:// so entering a bare host (e.g. "matrix.org") works. + val accountProviderUrl = event.accountProvider.trim().ensureProtocol() + submittedAccountProvider = accountProviderUrl changeServerState.eventSink( - ChangeServerEvents.ChangeServer(AccountProvider(url = event.accountProvider.trim())) + ChangeServerEvents.ChangeServer(AccountProvider(url = accountProviderUrl)) ) } ConfirmAccountProviderEvents.ClearError -> { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index 9cd288bfb1..9a2e2819cc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -88,12 +89,16 @@ fun ConfirmAccountProviderView( ?.substring(input.length) .orEmpty() + // The Continue button lives in a footer slot that is not recomposed while the user types, so a direct + // capture of the field text (via ::submit) goes stale and can submit an empty/prefilled value. Reading + // the value through rememberUpdatedState keeps it current regardless of which composition scope reads it. + val accountProviderToSubmit by rememberUpdatedState(input + suggestionSuffix) + fun submit() { // Dismiss the keyboard and release focus while the account provider is being validated. focusManager.clearFocus(force = true) - // 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)) + // Submit the exact field text (plus any accepted suggestion). + eventSink(ConfirmAccountProviderEvents.Continue(accountProviderToSubmit)) } // Once a validation / login error has been dismissed, return focus and the keyboard to the field so @@ -115,9 +120,9 @@ fun ConfirmAccountProviderView( iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), title = stringResource( id = if (state.isAccountCreation) { - CommonStrings.screen_select_server_title_register + CommonStrings.screen_change_server_title_register } else { - CommonStrings.screen_select_server_title_login + CommonStrings.screen_change_server_title_login } ), subTitle = null, @@ -153,13 +158,13 @@ fun ConfirmAccountProviderView( .focusRequester(focusRequester) .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.changeServerServer), - label = stringResource(id = CommonStrings.screen_select_server_textfield_header), - placeholder = stringResource(id = CommonStrings.screen_select_server_textfield_placeholder), + label = stringResource(id = CommonStrings.screen_change_server_textfield_header), + placeholder = stringResource(id = CommonStrings.screen_change_server_textfield_placeholder), supportingText = stringResource( id = if (state.isAccountCreation) { - CommonStrings.screen_select_server_textfield_footer_register + CommonStrings.screen_change_server_textfield_footer_register } else { - CommonStrings.screen_select_server_textfield_footer_login + CommonStrings.screen_change_server_textfield_footer_login } ), visualTransformation = ghostTransformation, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 687be70c7c..1bb983ff1a 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -349,6 +349,53 @@ class ConfirmAccountProviderPresenterTest { } } + @Test + fun `present - offers matrix_org as an autocomplete suggestion even without any history`() = runTest { + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = emptyList()), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.UserInputChanged("https://matr")) + val suggestionState = awaitState { it.accountProviderInput == "https://matr" } + assertThat(suggestionState.accountProviderSuggestion).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + } + } + + @Test + fun `present - offers matrix_org without the scheme as an autocomplete suggestion`() = runTest { + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = emptyList()), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.UserInputChanged("matr")) + val suggestionState = awaitState { it.accountProviderInput == "matr" } + assertThat(suggestionState.accountProviderSuggestion).isEqualTo("matrix.org") + } + } + + @Test + fun `present - continue prepends https to a bare account provider`() = runTest { + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue("matrix.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + cancelAndIgnoreRemainingEvents() + } + } + /** * Awaits until the emitted state's login mode matches [predicate], skipping the intermediate * account-provider validation states, and returns that state. diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index 2b5788042e..c704e811ff 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -10,10 +10,4 @@ "Collapsed" Active Inactive - "Enter your account provider" - "Choose an account provider" - "Account provider" - "example.com" - "If your account is provided by your company or a community, then change the account provider (example: companyname.com)." - "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org)."