From 2182840a71bc02aa194a89b03020f8beb8b288d8 Mon Sep 17 00:00:00 2001 From: Jenna Vassar Date: Wed, 19 Aug 2026 11:49:43 -0700 Subject: [PATCH] Let users enter their full Matrix user ID on the account provider screen --- .../ConfirmAccountProviderPresenter.kt | 18 ++++- .../ConfirmAccountProviderPresenterTest.kt | 72 +++++++++++++++++++ .../auth/FakeMatrixAuthenticationService.kt | 5 ++ 3 files changed, 93 insertions(+), 2 deletions(-) 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 21bc9077dd..5c66f8f446 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 @@ -30,6 +30,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.Presenter import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.preferences.api.store.AppPreferencesStore @AssistedInject @@ -87,6 +88,11 @@ class ConfirmAccountProviderPresenter( var submittedAccountProvider by remember { mutableStateOf(null) } val latestSubmittedAccountProvider by rememberUpdatedState(submittedAccountProvider) + // A login hint (the full Matrix user ID) captured when the user enters one, so the authentication + // server can pre-fill their identity. Null when a plain account provider was entered. + var submittedLoginHint by remember { mutableStateOf(null) } + val latestSubmittedLoginHint by rememberUpdatedState(submittedLoginHint) + // Once the chosen account provider has been validated and persisted, proceed with the actual sign in / sign up. // Reuse the details resolved while validating, so login does not configure (and re-network) the homeserver again. LaunchedEffect(changeServerState.changeServerAction) { @@ -97,7 +103,7 @@ class ConfirmAccountProviderPresenter( isAccountCreation = params.isAccountCreation, homeserverUrl = latestSubmittedAccountProvider.orEmpty().trim(), resolvedHomeserverUrl = null, - loginHint = null, + loginHint = latestSubmittedLoginHint, preConfiguredDetails = homeServerDetails, ) ) @@ -116,8 +122,16 @@ class ConfirmAccountProviderPresenter( // renders the full server rather than the typed prefix, and survives the OAuth round-trip. val accountProvider = event.accountProvider.trim() userInput = accountProvider + // If the user entered a full Matrix user ID (e.g. "@alice:example.org"), sign in to its + // homeserver and pass the ID as a login hint so the authentication server can pre-fill it. + // Otherwise treat the input as the account provider host. Only the homeserver is ever + // persisted to history, never the user ID. + val isUserId = MatrixPatterns.isUserId(accountProvider) + // For a user ID the homeserver is the domain part (after the ':'), keeping any port. + val host = if (isUserId) accountProvider.substringAfter(":") else accountProvider + submittedLoginHint = "mxid:$accountProvider".takeIf { isUserId && !params.isAccountCreation } // Default the scheme to https:// so entering a bare host (e.g. "matrix.org") works. - val accountProviderUrl = accountProvider.ensureProtocol() + val accountProviderUrl = host.ensureProtocol() submittedAccountProvider = accountProviderUrl changeServerState.eventSink( ChangeServerEvents.ChangeServer(AccountProvider(url = accountProviderUrl)) 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 1d9c32c0a0..641eea1563 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 @@ -460,6 +460,78 @@ class ConfirmAccountProviderPresenterTest { } } + @Test + fun `present - continue with a full matrix id signs in to its homeserver and passes a login hint`() = 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("@alice:example.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo("https://example.org") + assertThat(authenticationService.getOAuthUrlLoginHint).isEqualTo("mxid:@alice:example.org") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a matrix id keeps the port in the homeserver`() = 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("@alice:example.org:8448")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo("https://example.org:8448") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a plain account provider passes no login hint`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { 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(authenticationService.getOAuthUrlLoginHint).isNull() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a matrix id during account creation passes no login hint`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvents.Continue("@alice:example.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(authenticationService.getOAuthUrlLoginHint).isNull() + 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/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt index 3b2fdeafb1..1eac6c0a1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -70,10 +70,15 @@ class FakeMatrixAuthenticationService( return importCreatedSessionLambda(externalSession) } + /** The login hint passed to the most recent [getOAuthUrl] call. */ + var getOAuthUrlLoginHint: String? = null + private set + override suspend fun getOAuthUrl( prompt: OAuthPrompt, loginHint: String?, ): Result = simulateLongTask { + getOAuthUrlLoginHint = loginHint oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA) }