Let users enter their full Matrix user ID on the account provider screen

This commit is contained in:
Jenna Vassar
2026-08-19 11:49:43 -07:00
parent feef42db5a
commit 2182840a71
3 changed files with 93 additions and 2 deletions
@@ -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<String?>(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<String?>(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))
@@ -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<String>()
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<String>()
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.
@@ -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<OAuthDetails> = simulateLongTask {
getOAuthUrlLoginHint = loginHint
oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA)
}