Merge branch 'develop' into feature/bma/removeExternalCallSupport

This commit is contained in:
Benoit Marty 2026-04-30 11:50:35 +02:00
commit b81b1cd79b
262 changed files with 1644 additions and 825 deletions

View file

@ -17,6 +17,7 @@ jobs:
permissions: permissions:
# Need write permissions on PRs to remove the label "Record-Screenshots" # Need write permissions on PRs to remove the label "Record-Screenshots"
pull-requests: write pull-requests: write
contents: write
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }} name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots' if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'

View file

@ -103,13 +103,13 @@ android {
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]") logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]")
buildTypes { buildTypes {
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" val oAuthRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
getByName("debug") { getByName("debug") {
resValue("string", "app_name", "$baseAppName dbg") resValue("string", "app_name", "$baseAppName dbg")
resValue( resValue(
"string", "string",
"login_redirect_scheme", "login_redirect_scheme",
"$oidcRedirectSchemeBase.debug", "$oAuthRedirectSchemeBase.debug",
) )
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
@ -120,7 +120,7 @@ android {
resValue( resValue(
"string", "string",
"login_redirect_scheme", "login_redirect_scheme",
oidcRedirectSchemeBase, oAuthRedirectSchemeBase,
) )
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
@ -157,7 +157,7 @@ android {
resValue( resValue(
"string", "string",
"login_redirect_scheme", "login_redirect_scheme",
"$oidcRedirectSchemeBase.nightly", "$oAuthRedirectSchemeBase.nightly",
) )
matchingFallbacks += listOf("release") matchingFallbacks += listOf("release")
signingConfig = signingConfigs.getByName("nightly") signingConfig = signingConfigs.getByName("nightly")

View file

@ -75,7 +75,7 @@
android:scheme="elementx" /> android:scheme="elementx" />
</intent-filter> </intent-filter>
<!-- <!--
Oidc redirection OAuth redirection
--> -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View file

@ -10,14 +10,14 @@ package io.element.android.x.oidc
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.x.R import io.element.android.x.R
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultOidcRedirectUrlProvider( class DefaultOAuthRedirectUrlProvider(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
) : OidcRedirectUrlProvider { ) : OAuthRedirectUrlProvider {
override fun provide() = buildString { override fun provide() = buildString {
append(stringProvider.getString(R.string.login_redirect_scheme)) append(stringProvider.getString(R.string.login_redirect_scheme))
append(":/") append(":/")

View file

@ -13,13 +13,13 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.x.R import io.element.android.x.R
import org.junit.Test import org.junit.Test
class DefaultOidcRedirectUrlProviderTest { class DefaultOAuthRedirectUrlProviderTest {
@Test @Test
fun `test provide`() { fun `test provide`() {
val stringProvider = FakeStringProvider( val stringProvider = FakeStringProvider(
defaultResult = "str" defaultResult = "str"
) )
val sut = DefaultOidcRedirectUrlProvider( val sut = DefaultOAuthRedirectUrlProvider(
stringProvider = stringProvider, stringProvider = stringProvider,
) )
val result = sut.provide() val result = sut.provide()

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
object ProtectionConfig {
/**
* The maximum length of a room name, to limit attack vectors in room invite.
*/
const val MAX_ROOM_NAME_LENGTH = 128
}

View file

@ -33,7 +33,7 @@ dependencies {
implementation(projects.libraries.deeplink.api) implementation(projects.libraries.deeplink.api)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oauth.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
@ -59,7 +59,7 @@ dependencies {
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test) testImplementation(projects.features.share.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)

View file

@ -63,8 +63,8 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.ui.common.nodes.emptyNode import io.element.android.libraries.ui.common.nodes.emptyNode
@ -95,7 +95,7 @@ class RootFlowNode(
private val signedOutEntryPoint: SignedOutEntryPoint, private val signedOutEntryPoint: SignedOutEntryPoint,
private val accountSelectEntryPoint: AccountSelectEntryPoint, private val accountSelectEntryPoint: AccountSelectEntryPoint,
private val intentResolver: IntentResolver, private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow, private val oAuthActionFlow: OAuthActionFlow,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService, private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
@ -392,7 +392,7 @@ class RootFlowNode(
navigateTo(resolvedIntent.deeplinkData) navigateTo(resolvedIntent.deeplinkData)
} }
is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params) is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.OAuth -> onOAuthAction(resolvedIntent.oAuthAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData)
} }
@ -529,8 +529,8 @@ class RootFlowNode(
} }
} }
private fun onOidcAction(oidcAction: OidcAction) { private fun onOAuthAction(oAuthAction: OAuthAction) {
oidcActionFlow.post(oidcAction) oAuthActionFlow.post(oAuthAction)
} }
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {

View file

@ -18,13 +18,13 @@ import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.api.DeeplinkParser import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.api.OidcIntentResolver import io.element.android.libraries.oauth.api.OAuthIntentResolver
import timber.log.Timber import timber.log.Timber
sealed interface ResolvedIntent { sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent data class OAuth(val oAuthAction: OAuthAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
data class Login(val params: LoginParams) : ResolvedIntent data class Login(val params: LoginParams) : ResolvedIntent
data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
@ -34,7 +34,7 @@ sealed interface ResolvedIntent {
class IntentResolver( class IntentResolver(
private val deeplinkParser: DeeplinkParser, private val deeplinkParser: DeeplinkParser,
private val loginIntentResolver: LoginIntentResolver, private val loginIntentResolver: LoginIntentResolver,
private val oidcIntentResolver: OidcIntentResolver, private val oAuthIntentResolver: OAuthIntentResolver,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
private val shareIntentHandler: ShareIntentHandler, private val shareIntentHandler: ShareIntentHandler,
) { ) {
@ -45,9 +45,9 @@ class IntentResolver(
val deepLinkData = deeplinkParser.getFromIntent(intent) val deepLinkData = deeplinkParser.getFromIntent(intent)
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData) if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
// Coming during login using Oidc? // Coming during login using OAuth?
val oidcAction = oidcIntentResolver.resolve(intent) val oAuthAction = oAuthIntentResolver.resolve(intent)
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) if (oAuthAction != null) return ResolvedIntent.OAuth(oAuthAction)
val actionViewData = intent val actionViewData = intent
.takeIf { it.action == Intent.ACTION_VIEW } .takeIf { it.action == Intent.ACTION_VIEW }

View file

@ -26,8 +26,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -170,9 +170,9 @@ class IntentResolverTest {
} }
@Test @Test
fun `test resolve oidc`() { fun `test resolve OAuth`() {
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { OidcAction.GoBack() }, oAuthIntentResolverResult = { OAuthAction.GoBack() },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -180,8 +180,8 @@ class IntentResolverTest {
} }
val result = sut.resolve(intent) val result = sut.resolve(intent)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
ResolvedIntent.Oidc( ResolvedIntent.OAuth(
oidcAction = OidcAction.GoBack() oAuthAction = OAuthAction.GoBack()
) )
) )
} }
@ -194,7 +194,7 @@ class IntentResolverTest {
val sut = createIntentResolver( val sut = createIntentResolver(
loginIntentResolverResult = { null }, loginIntentResolverResult = { null },
permalinkParserResult = { permalinkData }, permalinkParserResult = { permalinkData },
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -213,7 +213,7 @@ class IntentResolverTest {
val sut = createIntentResolver( val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null }, loginIntentResolverResult = { null },
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -230,7 +230,7 @@ class IntentResolverTest {
) )
val sut = createIntentResolver( val sut = createIntentResolver(
permalinkParserResult = { permalinkData }, permalinkParserResult = { permalinkData },
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_BATTERY_LOW action = Intent.ACTION_BATTERY_LOW
@ -244,7 +244,7 @@ class IntentResolverTest {
fun `test incoming share simple`() { fun `test incoming share simple`() {
val shareIntentData = ShareIntentData.PlainText("Hello") val shareIntentData = ShareIntentData.PlainText("Hello")
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData }, onIncomingShareIntent = { shareIntentData },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
@ -260,7 +260,7 @@ class IntentResolverTest {
val fileUri = "content://com.example.app/file1.jpg".toUri() val fileUri = "content://com.example.app/file1.jpg".toUri()
val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg"))) val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg")))
val sut = createIntentResolver( val sut = createIntentResolver(
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
onIncomingShareIntent = { shareIntentData }, onIncomingShareIntent = { shareIntentData },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
@ -277,7 +277,7 @@ class IntentResolverTest {
val sut = createIntentResolver( val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
loginIntentResolverResult = { null }, loginIntentResolverResult = { null },
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -292,7 +292,7 @@ class IntentResolverTest {
val aLoginParams = LoginParams("accountProvider", null) val aLoginParams = LoginParams("accountProvider", null)
val sut = createIntentResolver( val sut = createIntentResolver(
loginIntentResolverResult = { aLoginParams }, loginIntentResolverResult = { aLoginParams },
oidcIntentResolverResult = { null }, oAuthIntentResolverResult = { null },
) )
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -306,7 +306,7 @@ class IntentResolverTest {
deeplinkParserResult: DeeplinkData? = null, deeplinkParserResult: DeeplinkData? = null,
permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, oAuthIntentResolverResult: (Intent) -> OAuthAction? = { lambdaError() },
onIncomingShareIntent: (Intent) -> ShareIntentData? = { null }, onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
@ -314,8 +314,8 @@ class IntentResolverTest {
loginIntentResolver = FakeLoginIntentResolver( loginIntentResolver = FakeLoginIntentResolver(
parseResult = loginIntentResolverResult, parseResult = loginIntentResolverResult,
), ),
oidcIntentResolver = FakeOidcIntentResolver( oAuthIntentResolver = FakeOAuthIntentResolver(
resolveResult = oidcIntentResolverResult, resolveResult = oAuthIntentResolverResult,
), ),
permalinkParser = FakePermalinkParser( permalinkParser = FakePermalinkParser(
result = permalinkParserResult result = permalinkParserResult

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState

View file

@ -1,4 +1,4 @@
This file contains some rough notes about Oidc implementation, with some examples of actual data. This file contains some rough notes about OAuth implementation, with some examples of actual data.
[ios implementation](https://github.com/element-hq/element-x-ios/compare/develop...doug/oidc-temp) [ios implementation](https://github.com/element-hq/element-x-ios/compare/develop...doug/oidc-temp)
@ -25,7 +25,7 @@ tosUri = "https://element.io/user-terms-of-service",
policyUri = "https://element.io/privacy" policyUri = "https://element.io/privacy"
Example of OidcData (from presentUrl callback): Example of OAuthData (from presentUrl callback):
url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent
Formatted url: Formatted url:
@ -43,8 +43,8 @@ https://auth-oidc.lab.element.dev/authorize?
state: ex6mNJVFZ5jn9wL8 state: ex6mNJVFZ5jn9wL8
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs OAuth client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs OAuth sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
Test server: Test server:

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
internal sealed interface CallScreenBackPressAction {
data object DispatchEscapeToWebView : CallScreenBackPressAction
data object EnterPictureInPicture : CallScreenBackPressAction
}
internal object CallScreenBackPressPolicy {
fun resolve(
supportPip: Boolean,
hasWebView: Boolean,
fromNative: Boolean,
): CallScreenBackPressAction? {
return when {
hasWebView && fromNative -> CallScreenBackPressAction.DispatchEscapeToWebView
hasWebView && supportPip -> CallScreenBackPressAction.EnterPictureInPicture
else -> null
}
}
}

View file

@ -64,11 +64,15 @@ internal fun CallScreenView(
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit, requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun handleBack() { var callWebView by remember { mutableStateOf<WebView?>(null) }
if (pipState.supportPip) {
pipState.eventSink.invoke(PictureInPictureEvent.EnterPictureInPicture) fun handleBack(fromNative: Boolean = false) {
} else { when (CallScreenBackPressPolicy.resolve(supportPip = pipState.supportPip, hasWebView = callWebView != null, fromNative)) {
state.eventSink(CallScreenEvent.Hangup) CallScreenBackPressAction.EnterPictureInPicture ->
pipState.eventSink(PictureInPictureEvent.EnterPictureInPicture)
CallScreenBackPressAction.DispatchEscapeToWebView ->
callWebView?.dispatchEscKeyEvent()
null -> Timber.d("Back press with unsupported pip is a no-op")
} }
} }
@ -76,7 +80,7 @@ internal fun CallScreenView(
modifier = modifier, modifier = modifier,
) { padding -> ) { padding ->
BackHandler { BackHandler {
handleBack() handleBack(fromNative = true)
} }
if (state.webViewError != null) { if (state.webViewError != null) {
ErrorDialog( ErrorDialog(
@ -111,6 +115,7 @@ internal fun CallScreenView(
}, },
onConsoleMessage = onConsoleMessage, onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView -> onCreateWebView = { webView ->
callWebView = webView
webView.addBackHandler(onBackPressed = ::handleBack) webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor( val interceptor = WebViewWidgetMessageInterceptor(
webView = webView, webView = webView,
@ -135,6 +140,7 @@ internal fun CallScreenView(
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController)) pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
}, },
onDestroyWebView = { onDestroyWebView = {
callWebView = null
// Reset audio mode // Reset audio mode
webViewAudioManager?.onCallStopped() webViewAudioManager?.onCallStopped()
} }
@ -143,6 +149,7 @@ internal fun CallScreenView(
AsyncData.Uninitialized, AsyncData.Uninitialized,
is AsyncData.Loading -> is AsyncData.Loading ->
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
is AsyncData.Failure -> { is AsyncData.Failure -> {
Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}") Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}")
ErrorDialog( ErrorDialog(
@ -150,6 +157,7 @@ internal fun CallScreenView(
onSubmit = { state.eventSink(CallScreenEvent.Hangup) }, onSubmit = { state.eventSink(CallScreenEvent.Hangup) },
) )
} }
is AsyncData.Success -> Unit is AsyncData.Success -> Unit
} }
} }
@ -248,15 +256,18 @@ private fun WebView.setup(
private fun WebView.addBackHandler(onBackPressed: () -> Unit) { private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
addJavascriptInterface( addJavascriptInterface(
object { JavascriptBackHandler {
@Suppress("unused") onBackPressed()
@JavascriptInterface
fun onBackPressed() = onBackPressed()
}, },
"backHandler" "backHandler"
) )
} }
private fun WebView.dispatchEscKeyEvent() {
dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_ESCAPE))
dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_ESCAPE))
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun CallScreenViewPreview( internal fun CallScreenViewPreview(
@ -275,3 +286,8 @@ internal fun CallScreenViewPreview(
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview { internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {} InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
} }
internal fun interface JavascriptBackHandler {
@JavascriptInterface
fun onBackPressed()
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.ui
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.ui.CallScreenBackPressAction
import io.element.android.features.call.impl.ui.CallScreenBackPressPolicy
import org.junit.Test
class CallScreenBackPressPolicyTest {
@Test
fun `resolve returns dispatch escape when a web view is available and native button is pressed`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = false,
hasWebView = true,
fromNative = true,
)
assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView)
}
@Test
fun `resolve dispatch escape when there is a web view and pip is supported on native button press`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = true,
hasWebView = true,
fromNative = true,
)
assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView)
}
@Test
fun `resolve returns hangup when there is no web view and pip is not supported from native button`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = false,
hasWebView = false,
fromNative = true,
)
assertThat(result).isNull()
}
@Test
fun `resolve returns hangup when there is no web view even though pip is supported from native button`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = true,
hasWebView = false,
fromNative = true,
)
assertThat(result).isNull()
}
@Test
fun `resolve goes to pip if its not from native but from the webview`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = true,
hasWebView = true,
fromNative = false,
)
assertThat(result).isEqualTo(CallScreenBackPressAction.EnterPictureInPicture)
}
@Test
fun `resolve hangs up if its not from native but from the webview and pip is not supported`() {
val result = CallScreenBackPressPolicy.resolve(
supportPip = false,
hasWebView = true,
fromNative = false,
)
assertThat(result).isNull()
}
@Test
fun `invalid cases (event comes from webview but there is now webview) all result in hangup`() {
val withPipSupport = CallScreenBackPressPolicy.resolve(
supportPip = true,
hasWebView = false,
fromNative = false,
)
assertThat(withPipSupport).isNull()
val withOutPipSupport = CallScreenBackPressPolicy.resolve(
supportPip = false,
hasWebView = false,
fromNative = false,
)
assertThat(withOutPipSupport).isNull()
}
}

View file

@ -0,0 +1,150 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.ui
import android.view.KeyEvent
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.ui.CallScreenEvents
import io.element.android.features.call.impl.ui.CallScreenView
import io.element.android.features.call.impl.ui.JavascriptBackHandler
import io.element.android.features.call.impl.ui.aCallScreenState
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.pressBackKey
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.robolectric.annotation.Resetter
import org.robolectric.shadows.ShadowWebView
@RunWith(AndroidJUnit4::class)
class CallScreenViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() {
val callEvents = EventsRecorder<CallScreenEvents>()
rule.setCallScreenView(
state = aCallScreenState(eventSink = callEvents),
useInspectionMode = true,
)
rule.pressBackKey()
callEvents.assertEmpty()
}
@Config(shadows = [RecordingShadowWebView::class])
@Test
fun `pressing back key dispatches escape key events to web view when pip is unsupported`() {
rule.setCallScreenView(
state = aCallScreenState(),
useInspectionMode = false,
)
rule.pressBackKey()
val dispatchedEvents = RecordingShadowWebView.dispatchedEvents
assertEquals(2, dispatchedEvents.size)
assertEquals(KeyEvent.ACTION_DOWN, dispatchedEvents[0].action)
assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[0].keyCode)
assertEquals(KeyEvent.ACTION_UP, dispatchedEvents[1].action)
assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[1].keyCode)
}
@Config(shadows = [RecordingShadowWebView::class])
@Test
fun `web view javascript back handler emits pip event when pip is supported`() {
val pipEvents = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
state = aCallScreenState(),
useInspectionMode = false,
pipState = aPictureInPictureState(
supportPip = true,
eventSink = pipEvents,
),
)
rule.runOnIdle {
RecordingShadowWebView.invokeJavascriptBackHandler()
}
pipEvents.assertSize(2)
pipEvents.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
pipEvents.assertTrue(1) { it is PictureInPictureEvents.EnterPictureInPicture }
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView(
state: io.element.android.features.call.impl.ui.CallScreenState,
useInspectionMode: Boolean,
pipState: io.element.android.features.call.impl.pip.PictureInPictureState = aPictureInPictureState(supportPip = false),
) {
setContent {
// Inspection mode disables AndroidView creation; keep it configurable per test.
CompositionLocalProvider(LocalInspectionMode provides useInspectionMode) {
CallScreenView(
state = state,
pipState = pipState,
onConsoleMessage = {},
requestPermissions = { _, _ -> },
)
}
}
}
@Implements(WebView::class)
internal class RecordingShadowWebView : ShadowWebView() {
companion object {
val dispatchedEvents = mutableListOf<KeyEvent>()
private var backHandlerJavascriptInterface: JavascriptBackHandler? = null
@Resetter
@JvmStatic
@Suppress("unused")
fun resetRecordedEvents() {
dispatchedEvents.clear()
backHandlerJavascriptInterface = null
}
fun invokeJavascriptBackHandler() {
val backHandler = checkNotNull(backHandlerJavascriptInterface) { "Expected backHandler JavaScript interface to be registered" }
backHandler.onBackPressed()
}
}
@Implementation
protected override fun addJavascriptInterface(`object`: Any, name: String) {
super.addJavascriptInterface(`object`, name)
if (name == "backHandler") {
backHandlerJavascriptInterface = `object` as? JavascriptBackHandler
}
}
@Implementation
@Suppress("unused")
fun dispatchKeyEvent(event: KeyEvent): Boolean {
dispatchedEvents += KeyEvent(event)
return false
}
}

View file

@ -95,7 +95,8 @@ internal fun SelectParentSpaceOptions(
sheetState.hide(coroutineScope) { sheetState.hide(coroutineScope) {
displaySelectSpaceBottomSheet = false displaySelectSpaceBottomSheet = false
} }
} },
scrollable = false,
) { ) {
SelectParentSpaceBottomSheet( SelectParentSpaceBottomSheet(
spaces = spaces, spaces = spaces,

View file

@ -11,6 +11,8 @@ package io.element.android.features.home.impl.roomlist
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -43,6 +45,7 @@ fun RoomListContextMenu(
) { ) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { eventSink(RoomListEvent.HideContextMenu) }, onDismissRequest = { eventSink(RoomListEvent.HideContextMenu) },
scrollable = false,
) { ) {
RoomListModalBottomSheetContent( RoomListModalBottomSheetContent(
contextMenu = contextMenu, contextMenu = contextMenu,
@ -91,7 +94,9 @@ private fun RoomListModalBottomSheetContent(
onReportRoomClick: () -> Unit, onReportRoomClick: () -> Unit,
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) { ) {
ListItem( ListItem(
headlineContent = { headlineContent = {
@ -212,23 +217,16 @@ private fun RoomListModalBottomSheetContent(
} }
} }
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
// see: https://issuetracker.google.com/issues/283843380
// Remove this preview when the issue is fixed.
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun RoomListModalBottomSheetContentPreview( internal fun RoomListContextMenuPreview(
@PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown @PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
) = ElementPreview { ) = ElementPreview {
RoomListModalBottomSheetContent( RoomListContextMenu(
contextMenu = contextMenu, contextMenu = contextMenu,
canReportRoom = true, canReportRoom = true,
onRoomMarkReadClick = {},
onRoomMarkUnreadClick = {},
onRoomSettingsClick = {}, onRoomSettingsClick = {},
onLeaveRoomClick = {},
onFavoriteChange = {},
onClearCacheRoomClick = {},
onReportRoomClick = {}, onReportRoomClick = {},
eventSink = {},
) )
} }

View file

@ -13,16 +13,21 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.appconfig.ProtectionConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.home.impl.R import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.core.extensions.toSafeLength
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -42,9 +47,14 @@ fun RoomListDeclineInviteMenu(
) { ) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { eventSink(RoomListEvent.HideDeclineInviteMenu) }, onDismissRequest = { eventSink(RoomListEvent.HideDeclineInviteMenu) },
scrollable = false,
) { ) {
RoomListDeclineInviteMenuContent( RoomListDeclineInviteMenuContent(
roomName = menu.roomSummary.name ?: menu.roomSummary.roomId.value, roomName = menu.roomSummary.name?.toSafeLength(
maxLength = ProtectionConfig.MAX_ROOM_NAME_LENGTH,
ellipsize = true,
)
?: menu.roomSummary.roomId.value,
onDeclineClick = { onDeclineClick = {
eventSink(RoomListEvent.HideDeclineInviteMenu) eventSink(RoomListEvent.HideDeclineInviteMenu)
eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, false)) eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, false))
@ -74,7 +84,8 @@ private fun RoomListDeclineInviteMenuContent(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp), .padding(all = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
@ -112,16 +123,15 @@ private fun RoomListDeclineInviteMenuContent(
} }
} }
// TODO This component should be seen in [RoomListView] @Preview but it doesn't show up.
// see: https://issuetracker.google.com/issues/283843380
// Remove this preview when the issue is fixed.
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun RoomListDeclineInviteMenuContentPreview() = ElementPreview { internal fun RoomListDeclineInviteMenuPreview(
RoomListDeclineInviteMenuContent( @PreviewParameter(RoomListStateDeclineInviteMenuShownProvider::class) menu: RoomListState.DeclineInviteMenu.Shown,
roomName = "Room name", ) = ElementPreview {
onCancelClick = {}, RoomListDeclineInviteMenu(
onDeclineClick = {}, menu = menu,
canReportRoom = false,
onDeclineAndBlockClick = {}, onDeclineAndBlockClick = {},
eventSink = {},
) )
} }

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.aRoomListRoomSummary
open class RoomListStateDeclineInviteMenuShownProvider : PreviewParameterProvider<RoomListState.DeclineInviteMenu.Shown> {
override val values: Sequence<RoomListState.DeclineInviteMenu.Shown>
get() = sequenceOf(
aDeclineInviteMenuShown(),
aDeclineInviteMenuShown(
aRoomListRoomSummary(
name = LoremIpsum(500).values.first(),
)
),
aDeclineInviteMenuShown(
aRoomListRoomSummary(
name = null,
)
),
)
}
internal fun aDeclineInviteMenuShown(
roomSummary: RoomListRoomSummary = aRoomListRoomSummary(),
) = RoomListState.DeclineInviteMenu.Shown(
roomSummary = roomSummary,
)

View file

@ -81,7 +81,8 @@ fun SpaceFiltersView(
if (state is SpaceFiltersState.Selecting) { if (state is SpaceFiltersState.Selecting) {
state.eventSink(SpaceFiltersEvent.Selecting.Cancel) state.eventSink(SpaceFiltersEvent.Selecting.Cancel)
} }
} },
scrollable = false,
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier

View file

@ -262,6 +262,7 @@ private fun InvitePeopleConfirmModal(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
dragHandle = null, dragHandle = null,
scrollable = false,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
title = simplePluralStringResource( title = simplePluralStringResource(

View file

@ -43,7 +43,7 @@ dependencies {
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.qrcode) implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oauth.api)
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.libraries.wellknown.api) implementation(projects.libraries.wellknown.api)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
@ -56,7 +56,7 @@ dependencies {
testImplementation(projects.features.enterprise.test) testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.wellknown.test) testImplementation(projects.libraries.wellknown.test)

View file

@ -27,6 +27,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode
import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode
import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode
import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType
@ -107,6 +108,11 @@ class LinkNewDeviceFlowNode(
val data: String, val data: String,
) : NavTarget ) : NavTarget
@Parcelize
data class CodeConfirmation(
val code: String,
) : NavTarget
@Parcelize @Parcelize
data object MobileEnterNumber : NavTarget data object MobileEnterNumber : NavTarget
@ -145,10 +151,7 @@ class LinkNewDeviceFlowNode(
LinkMobileStep.Starting -> { LinkMobileStep.Starting -> {
// This step is not received at the moment, so do nothing // This step is not received at the moment, so do nothing
} }
LinkMobileStep.SyncingSecrets -> { LinkMobileStep.SyncingSecrets -> Unit
// LinkMobileStep.Done is not received at the moment, so consider that the flow is done here
callback.onDone()
}
is LinkMobileStep.WaitingForAuth -> { is LinkMobileStep.WaitingForAuth -> {
navigateToBrowser(linkMobileStep.verificationUri) navigateToBrowser(linkMobileStep.verificationUri)
} }
@ -166,7 +169,9 @@ class LinkNewDeviceFlowNode(
is LinkDesktopStep.Error -> { is LinkDesktopStep.Error -> {
navigateToError(linkDesktopStep.errorType) navigateToError(linkDesktopStep.errorType)
} }
is LinkDesktopStep.EstablishingSecureChannel -> Unit is LinkDesktopStep.EstablishingSecureChannel -> {
backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString))
}
is LinkDesktopStep.InvalidQrCode -> { is LinkDesktopStep.InvalidQrCode -> {
// This error will be handled by the ScanQrCodeNode // This error will be handled by the ScanQrCodeNode
} }
@ -183,20 +188,20 @@ class LinkNewDeviceFlowNode(
private fun navigateToError(errorType: ErrorType) { private fun navigateToError(errorType: ErrorType) {
// Map the error to an error screen // Map the error to an error screen
// TODO Update this mapping
val error = when (errorType) { val error = when (errorType) {
is ErrorType.DeviceIdAlreadyInUse -> ErrorScreenType.UnknownError is ErrorType.InvalidCheckCode -> ErrorScreenType.Mismatch2Digits
is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected is ErrorType.UnsupportedProtocol -> ErrorScreenType.ProtocolNotSupported
is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError is ErrorType.Cancelled -> ErrorScreenType.Cancelled
is ErrorType.NotFound -> ErrorScreenType.Expired
is ErrorType.DeviceNotFound -> ErrorScreenType.UnknownError
is ErrorType.Unknown -> ErrorScreenType.UnknownError
is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError
is ErrorType.Cancelled -> ErrorScreenType.UnknownError
is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected
is ErrorType.Expired -> ErrorScreenType.Expired is ErrorType.Expired,
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError is ErrorType.NotFound,
is ErrorType.DeviceNotFound -> ErrorScreenType.Expired
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.OtherDeviceAlreadySignedIn
// TODO check if we expect to hit this here or if it should be caught earlier on
is ErrorType.UnsupportedQrCodeType -> ErrorScreenType.UnknownError is ErrorType.UnsupportedQrCodeType -> ErrorScreenType.UnknownError
is ErrorType.MissingSecretsBackup,
is ErrorType.DeviceIdAlreadyInUse,
is ErrorType.Unknown -> ErrorScreenType.UnknownError
} }
// It is OK to push on backstack, since when user leaves the error screen, a new root will be set, // It is OK to push on backstack, since when user leaves the error screen, a new root will be set,
// or the whole flow will be popped. // or the whole flow will be popped.
@ -250,6 +255,18 @@ class LinkNewDeviceFlowNode(
} }
createNode<EnterNumberNode>(buildContext, listOf(callback)) createNode<EnterNumberNode>(buildContext, listOf(callback))
} }
is NavTarget.CodeConfirmation -> {
val callback = object : CodeConfirmationNode.Callback {
override fun onCancel() {
// Push error
backstack.push(NavTarget.Error(ErrorScreenType.Cancelled))
}
}
val inputs = CodeConfirmationNode.Inputs(
code = navTarget.code,
)
createNode<CodeConfirmationNode>(buildContext, listOf(inputs, callback))
}
is NavTarget.MobileShowQrCode -> { is NavTarget.MobileShowQrCode -> {
val callback = object : ShowQrCodeNode.Callback { val callback = object : ShowQrCodeNode.Callback {
override fun navigateBack() { override fun navigateBack() {

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.linknewdevice.impl.screens.confirmation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class CodeConfirmationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext = buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onCancel()
}
data class Inputs(
val code: String,
) : NodeInputs
private val callback: Callback = callback()
private val input = inputs<Inputs>()
@Composable
override fun View(modifier: Modifier) {
CodeConfirmationView(
code = input.code,
onCancel = callback::onCancel,
)
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.linknewdevice.impl.screens.confirmation
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.linknewdevice.impl.R
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun CodeConfirmationView(
code: String,
onCancel: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = onCancel)
FlowStepPage(
modifier = modifier,
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
title = stringResource(R.string.screen_qr_code_login_device_code_title),
subTitle = stringResource(R.string.screen_qr_code_login_device_code_subtitle),
content = { Content(code = code) },
buttons = { Buttons(onCancel = onCancel) }
)
}
@Composable
private fun Content(code: String) {
Column(
modifier = Modifier.padding(top = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Digits(code = code)
Spacer(modifier = Modifier.height(32.dp))
WaitingForOtherDevice()
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Digits(code: String) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
code.forEach {
Text(
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 4.dp)
.clip(RoundedCornerShape(4.dp))
.background(ElementTheme.colors.bgActionSecondaryPressed)
.padding(horizontal = 16.dp, vertical = 17.dp),
text = it.toString()
)
}
}
}
@Composable
private fun WaitingForOtherDevice() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
CircularProgressIndicator(
modifier = Modifier
.size(20.dp)
.padding(2.dp),
strokeWidth = 2.dp,
)
Text(
text = stringResource(R.string.screen_qr_code_login_verify_code_loading),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun Buttons(
onCancel: () -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_cancel),
onClick = onCancel,
)
}
}
@PreviewsDayNight
@Composable
internal fun CodeConfirmationViewPreview() {
ElementPreview {
CodeConfirmationView(
code = "67",
onCancel = {},
)
}
}

View file

@ -20,6 +20,9 @@ sealed interface ErrorScreenType : NodeInputs, Parcelable {
@Parcelize @Parcelize
data object Expired : ErrorScreenType data object Expired : ErrorScreenType
@Parcelize
data object OtherDeviceAlreadySignedIn : ErrorScreenType
@Parcelize @Parcelize
data object Mismatch2Digits : ErrorScreenType data object Mismatch2Digits : ErrorScreenType

View file

@ -19,5 +19,6 @@ class ErrorScreenTypeProvider : PreviewParameterProvider<ErrorScreenType> {
ErrorScreenType.InsecureChannelDetected, ErrorScreenType.InsecureChannelDetected,
ErrorScreenType.SlidingSyncNotAvailable, ErrorScreenType.SlidingSyncNotAvailable,
ErrorScreenType.UnknownError, ErrorScreenType.UnknownError,
ErrorScreenType.OtherDeviceAlreadySignedIn,
) )
} }

View file

@ -47,17 +47,26 @@ fun ErrorView(
) { ) {
val appName = LocalBuildMeta.current.applicationName val appName = LocalBuildMeta.current.applicationName
BackHandler(onBack = onCancel) BackHandler(onBack = onCancel)
val iconStyle = when (errorScreenType) {
ErrorScreenType.OtherDeviceAlreadySignedIn -> BigIcon.Style.SuccessSolid
else -> BigIcon.Style.AlertSolid
}
FlowStepPage( FlowStepPage(
modifier = modifier, modifier = modifier,
iconStyle = BigIcon.Style.AlertSolid, iconStyle = iconStyle,
title = titleText(errorScreenType, appName), title = titleText(errorScreenType, appName),
subTitle = subtitleText(errorScreenType, appName), subTitle = subtitleText(errorScreenType, appName),
content = { Content(errorScreenType) }, content = { Content(errorScreenType) },
buttons = { buttons = {
Buttons( when (errorScreenType) {
onRetry = onRetry, ErrorScreenType.OtherDeviceAlreadySignedIn -> DoneButton(
onCancel = onCancel, onDone = onCancel,
) )
else -> Buttons(
onRetry = onRetry,
onCancel = onCancel,
)
}
}, },
) )
} }
@ -72,6 +81,7 @@ private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when
ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_title) ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_title)
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName) ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName)
is ErrorScreenType.UnknownError -> stringResource(CommonStrings.common_something_went_wrong) is ErrorScreenType.UnknownError -> stringResource(CommonStrings.common_something_went_wrong)
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_title)
} }
@Composable @Composable
@ -84,6 +94,7 @@ private fun subtitleText(errorScreenType: ErrorScreenType, appName: String) = wh
ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description) ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description)
ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName) ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName)
is ErrorScreenType.UnknownError -> stringResource(R.string.screen_qr_code_login_unknown_error_description) is ErrorScreenType.UnknownError -> stringResource(R.string.screen_qr_code_login_unknown_error_description)
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_subtitle)
} }
@Composable @Composable
@ -124,6 +135,17 @@ private fun Content(errorScreenType: ErrorScreenType) {
} }
} }
@Composable
private fun DoneButton(
onDone: () -> Unit,
) {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_done),
onClick = onDone,
)
}
@Composable @Composable
private fun Buttons( private fun Buttons(
onRetry: () -> Unit, onRetry: () -> Unit,

View file

@ -34,6 +34,8 @@
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesnt work, sign in manually"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesnt work, sign in manually"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Youll be asked to enter the two digits shown on this device."</string>
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string> <string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string> <string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string> <string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string>
@ -54,4 +56,5 @@ Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your devices camera in order to continue."</string> <string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your devices camera in order to continue."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string> <string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
<string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string> <string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string>
<string name="screen_qr_code_login_verify_code_loading">"Waiting for your other device"</string>
</resources> </resources>

View file

@ -69,7 +69,7 @@ dependencies {
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.qrcode) implementation(projects.libraries.qrcode)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oauth.api)
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.libraries.wellknown.api) implementation(projects.libraries.wellknown.api)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
@ -83,7 +83,7 @@ dependencies {
testImplementation(projects.features.preferences.test) testImplementation(projects.features.preferences.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.wellknown.test) testImplementation(projects.libraries.wellknown.test)

View file

@ -50,9 +50,9 @@ import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,7 +64,7 @@ class LoginFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val accountProviderDataSource: AccountProviderDataSource, private val accountProviderDataSource: AccountProviderDataSource,
private val oidcActionFlow: OidcActionFlow, private val oAuthActionFlow: OAuthActionFlow,
@AppCoroutineScope @AppCoroutineScope
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val elementClassicConnection: ElementClassicConnection, private val elementClassicConnection: ElementClassicConnection,
@ -100,7 +100,7 @@ class LoginFlowNode(
// by pressing back or by closing the Custom Chrome Tab. // by pressing back or by closing the Custom Chrome Tab.
lifecycleScope.launch { lifecycleScope.launch {
delay(5000) delay(5000)
oidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) oAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
} }
} }
} }
@ -161,8 +161,8 @@ class LoginFlowNode(
backstack.push(NavTarget.LoginPassword()) backstack.push(NavTarget.LoginPassword())
} }
override fun navigateToOidc(oidcDetails: OidcDetails) { override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
navigateToMas(oidcDetails) navigateToMas(oAuthDetails)
} }
override fun navigateToCreateAccount(url: String) { override fun navigateToCreateAccount(url: String) {
@ -197,8 +197,8 @@ class LoginFlowNode(
callback.navigateToBugReport() callback.navigateToBugReport()
} }
override fun navigateToOidc(oidcDetails: OidcDetails) { override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
navigateToMas(oidcDetails) navigateToMas(oAuthDetails)
} }
override fun navigateToCreateAccount(url: String) { override fun navigateToCreateAccount(url: String) {
@ -243,8 +243,8 @@ class LoginFlowNode(
} }
NavTarget.ChooseAccountProvider -> { NavTarget.ChooseAccountProvider -> {
val callback = object : ChooseAccountProviderNode.Callback { val callback = object : ChooseAccountProviderNode.Callback {
override fun navigateToOidc(oidcDetails: OidcDetails) { override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
navigateToMas(oidcDetails) navigateToMas(oAuthDetails)
} }
override fun navigateToCreateAccount(url: String) { override fun navigateToCreateAccount(url: String) {
@ -270,8 +270,8 @@ class LoginFlowNode(
isAccountCreation = navTarget.isAccountCreation, isAccountCreation = navTarget.isAccountCreation,
) )
val callback = object : ConfirmAccountProviderNode.Callback { val callback = object : ConfirmAccountProviderNode.Callback {
override fun navigateToOidc(oidcDetails: OidcDetails) { override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
navigateToMas(oidcDetails) navigateToMas(oAuthDetails)
} }
override fun navigateToCreateAccount(url: String) { override fun navigateToCreateAccount(url: String) {
@ -333,10 +333,10 @@ class LoginFlowNode(
} }
} }
private fun navigateToMas(oidcDetails: OidcDetails) { private fun navigateToMas(oAuthDetails: OAuthDetails) {
activity?.let { activity?.let {
externalAppStarted = true externalAppStarted = true
it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url) it.openUrlInChromeCustomTab(null, darkTheme, oAuthDetails.url)
} }
} }

View file

@ -41,7 +41,7 @@ sealed class ChangeServerError : Exception() {
// AccountAlreadyLoggedIn error should not happen at this point // AccountAlreadyLoggedIn error should not happen at this point
is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message) is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message)
is AuthenticationException.Generic -> Error(messageStr = error.message) is AuthenticationException.Generic -> Error(messageStr = error.message)
is AuthenticationException.Oidc -> Error(messageStr = error.message) is AuthenticationException.OAuth -> Error(messageStr = error.message)
} }
} }
is AccountProviderAccessException.NeedElementProException -> NeedElementPro( is AccountProviderAccessException.NeedElementProException -> NeedElementPro(

View file

@ -23,9 +23,9 @@ import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationR
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcPrompt import io.element.android.libraries.matrix.api.auth.OAuthPrompt
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
/** /**
* This class is responsible for managing the login flow, including handling OIDC actions and * This class is responsible for managing the login flow, including handling OIDC actions and
@ -35,7 +35,7 @@ import io.element.android.libraries.oidc.api.OidcActionFlow
*/ */
@Inject @Inject
class LoginHelper( class LoginHelper(
private val oidcActionFlow: OidcActionFlow, private val oAuthActionFlow: OAuthActionFlow,
private val authenticationService: MatrixAuthenticationService, private val authenticationService: MatrixAuthenticationService,
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
) { ) {
@ -44,9 +44,9 @@ class LoginHelper(
@Composable @Composable
fun collectLoginMode(): State<AsyncData<LoginMode>> { fun collectLoginMode(): State<AsyncData<LoginMode>> {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
oidcActionFlow.collect { oidcAction -> oAuthActionFlow.collect { oAuthAction ->
if (oidcAction != null) { if (oAuthAction != null) {
onOidcAction(oidcAction) onOAuthAction(oAuthAction)
} }
} }
} }
@ -73,11 +73,11 @@ class LoginHelper(
throw it throw it
} }
}.map { matrixHomeServerDetails -> }.map { matrixHomeServerDetails ->
if (matrixHomeServerDetails.supportsOidcLogin) { if (matrixHomeServerDetails.supportsOAuthLogin) {
// Retrieve the details right now // Retrieve the details right now
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
LoginMode.Oidc( LoginMode.OAuth(
authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow() authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow()
) )
} else if (isAccountCreation) { } else if (isAccountCreation) {
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
@ -99,16 +99,16 @@ class LoginHelper(
) )
} }
private suspend fun onOidcAction(oidcAction: OidcAction) { private suspend fun onOAuthAction(oAuthAction: OAuthAction) {
if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) { if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
// Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode.
// This can happen if there is an error, for instance attempt to login again on the same account. // This can happen if there is an error, for instance attempt to login again on the same account.
return return
} }
loginModeState.value = AsyncData.Loading() loginModeState.value = AsyncData.Loading()
when (oidcAction) { when (oAuthAction) {
is OidcAction.GoBack -> { is OAuthAction.GoBack -> {
authenticationService.cancelOidcLogin() authenticationService.cancelOAuthLogin()
.onSuccess { .onSuccess {
loginModeState.value = AsyncData.Uninitialized loginModeState.value = AsyncData.Uninitialized
} }
@ -116,13 +116,13 @@ class LoginHelper(
loginModeState.value = AsyncData.Failure(failure) loginModeState.value = AsyncData.Failure(failure)
} }
} }
is OidcAction.Success -> { is OAuthAction.Success -> {
authenticationService.loginWithOidc(oidcAction.url) authenticationService.loginWithOAuth(oAuthAction.url)
.onFailure { failure -> .onFailure { failure ->
loginModeState.value = AsyncData.Failure(failure) loginModeState.value = AsyncData.Failure(failure)
} }
} }
} }
oidcActionFlow.reset() oAuthActionFlow.reset()
} }
} }

View file

@ -8,10 +8,10 @@
package io.element.android.features.login.impl.login package io.element.android.features.login.impl.login
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
sealed interface LoginMode { sealed interface LoginMode {
data object PasswordLogin : LoginMode data object PasswordLogin : LoginMode
data class Oidc(val oidcDetails: OidcDetails) : LoginMode data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode
data class AccountCreation(val url: String) : LoginMode data class AccountCreation(val url: String) : LoginMode
} }

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -32,7 +32,7 @@ fun LoginModeView(
loginMode: AsyncData<LoginMode>, loginMode: AsyncData<LoginMode>,
onClearError: () -> Unit, onClearError: () -> Unit,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit, onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit, onNeedLoginPassword: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit onCreateAccountContinue: (url: String) -> Unit
) { ) {
@ -118,7 +118,7 @@ fun LoginModeView(
is AsyncData.Loading -> Unit // The Continue button shows the loading state is AsyncData.Loading -> Unit // The Continue button shows the loading state
is AsyncData.Success -> { is AsyncData.Success -> {
when (val loginModeData = loginMode.data) { when (val loginModeData = loginMode.data) {
is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails) is LoginMode.OAuth -> onOAuthDetails(loginModeData.oAuthDetails)
LoginMode.PasswordLogin -> onNeedLoginPassword() LoginMode.PasswordLogin -> onNeedLoginPassword()
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url) is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
} }
@ -137,7 +137,7 @@ internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::
loginMode = AsyncData.Failure(error), loginMode = AsyncData.Failure(error),
onClearError = {}, onClearError = {},
onLearnMoreClick = {}, onLearnMoreClick = {},
onOidcDetails = {}, onOAuthDetails = {},
onNeedLoginPassword = {}, onNeedLoginPassword = {},
onCreateAccountContinue = {} onCreateAccountContinue = {}
) )

View file

@ -135,8 +135,8 @@ class QrCodeLoginFlowNode(
is QrLoginException.SlidingSyncNotAvailable -> { is QrLoginException.SlidingSyncNotAvailable -> {
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable)) backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable))
} }
is QrLoginException.OidcMetadataInvalid -> { is QrLoginException.OAuthMetadataInvalid -> {
Timber.e(error, "OIDC metadata is invalid") Timber.e(error, "OAuth metadata is invalid")
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError)) backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
} }
QrLoginException.CheckCodeAlreadySent, QrLoginException.CheckCodeAlreadySent,

View file

@ -20,7 +20,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -31,7 +31,7 @@ class ChooseAccountProviderNode(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToLoginPassword() fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToOAuth(oAuthDetails: OAuthDetails)
fun navigateToCreateAccount(url: String) fun navigateToCreateAccount(url: String)
} }
@ -45,7 +45,7 @@ class ChooseAccountProviderNode(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onOidcDetails = callback::navigateToOidc, onOAuthDetails = callback::navigateToOAuth,
onNeedLoginPassword = callback::navigateToLoginPassword, onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = callback::navigateToCreateAccount, onCreateAccountContinue = callback::navigateToCreateAccount,

View file

@ -43,14 +43,14 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun ChooseAccountProviderView( fun ChooseAccountProviderView(
state: ChooseAccountProviderState, state: ChooseAccountProviderState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit, onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit, onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
@ -129,7 +129,7 @@ fun ChooseAccountProviderView(
state.eventSink(ChooseAccountProviderEvents.ClearError) state.eventSink(ChooseAccountProviderEvents.ClearError)
}, },
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
@ -144,7 +144,7 @@ internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountPro
state = state, state = state,
onBackClick = { }, onBackClick = { },
onLearnMoreClick = { }, onLearnMoreClick = { },
onOidcDetails = { }, onOAuthDetails = { },
onNeedLoginPassword = { }, onNeedLoginPassword = { },
onCreateAccountContinue = { }, onCreateAccountContinue = { },
) )

View file

@ -31,7 +31,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.rememberFaderOrSliderTransitionHandler import io.element.android.libraries.architecture.appyx.rememberFaderOrSliderTransitionHandler
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -54,7 +54,7 @@ class ClassicFlowNode(
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToOnBoarding(allowBackNavigation: Boolean) fun navigateToOnBoarding(allowBackNavigation: Boolean)
fun navigateToLoginPassword() fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToOAuth(oAuthDetails: OAuthDetails)
fun navigateToCreateAccount(url: String) fun navigateToCreateAccount(url: String)
} }
@ -111,8 +111,8 @@ class ClassicFlowNode(
callback.navigateToLoginPassword() callback.navigateToLoginPassword()
} }
override fun navigateToOidc(oidcDetails: OidcDetails) { override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
callback.navigateToOidc(oidcDetails) callback.navigateToOAuth(oAuthDetails)
} }
override fun navigateToCreateAccount(url: String) { override fun navigateToCreateAccount(url: String) {

View file

@ -21,7 +21,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@ -35,7 +35,7 @@ class LoginWithClassicNode(
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToOtherOptions() fun navigateToOtherOptions()
fun navigateToLoginPassword() fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToOAuth(oAuthDetails: OAuthDetails)
fun navigateToCreateAccount(url: String) fun navigateToCreateAccount(url: String)
fun navigateToMissingKeyBackup() fun navigateToMissingKeyBackup()
} }
@ -60,7 +60,7 @@ class LoginWithClassicNode(
state = state, state = state,
modifier = modifier, modifier = modifier,
onOtherOptionsClick = callback::navigateToOtherOptions, onOtherOptionsClick = callback::navigateToOtherOptions,
onOidcDetails = callback::navigateToOidc, onOAuthDetails = callback::navigateToOAuth,
onNeedLoginPassword = callback::navigateToLoginPassword, onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = callback::navigateToCreateAccount, onCreateAccountContinue = callback::navigateToCreateAccount,

View file

@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun LoginWithClassicView( fun LoginWithClassicView(
state: LoginWithClassicState, state: LoginWithClassicState,
onOtherOptionsClick: () -> Unit, onOtherOptionsClick: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit, onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit, onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
@ -200,7 +200,7 @@ fun LoginWithClassicView(
state.eventSink(LoginWithClassicEvent.ClearError) state.eventSink(LoginWithClassicEvent.ClearError)
}, },
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
@ -212,7 +212,7 @@ internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicState
LoginWithClassicView( LoginWithClassicView(
state = state, state = state,
onOtherOptionsClick = {}, onOtherOptionsClick = {},
onOidcDetails = {}, onOAuthDetails = {},
onNeedLoginPassword = {}, onNeedLoginPassword = {},
onLearnMoreClick = {}, onLearnMoreClick = {},
onCreateAccountContinue = {}, onCreateAccountContinue = {},

View file

@ -22,7 +22,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -44,7 +44,7 @@ class ConfirmAccountProviderNode(
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToLoginPassword() fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToOAuth(oAuthDetails: OAuthDetails)
fun navigateToCreateAccount(url: String) fun navigateToCreateAccount(url: String)
fun navigateToChangeAccountProvider() fun navigateToChangeAccountProvider()
} }
@ -58,7 +58,7 @@ class ConfirmAccountProviderNode(
ConfirmAccountProviderView( ConfirmAccountProviderView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onOidcDetails = callback::navigateToOidc, onOAuthDetails = callback::navigateToOAuth,
onNeedLoginPassword = callback::navigateToLoginPassword, onNeedLoginPassword = callback::navigateToLoginPassword,
onCreateAccountContinue = callback::navigateToCreateAccount, onCreateAccountContinue = callback::navigateToCreateAccount,
onChange = callback::navigateToChangeAccountProvider, onChange = callback::navigateToChangeAccountProvider,

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -38,7 +38,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun ConfirmAccountProviderView( fun ConfirmAccountProviderView(
state: ConfirmAccountProviderState, state: ConfirmAccountProviderState,
onOidcDetails: (OidcDetails) -> Unit, onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit, onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
@ -103,7 +103,7 @@ fun ConfirmAccountProviderView(
eventSink(ConfirmAccountProviderEvents.ClearError) eventSink(ConfirmAccountProviderEvents.ClearError)
}, },
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
@ -117,7 +117,7 @@ internal fun ConfirmAccountProviderViewPreview(
) = ElementPreview { ) = ElementPreview {
ConfirmAccountProviderView( ConfirmAccountProviderView(
state = state, state = state,
onOidcDetails = {}, onOAuthDetails = {},
onNeedLoginPassword = {}, onNeedLoginPassword = {},
onCreateAccountContinue = {}, onCreateAccountContinue = {},
onLearnMoreClick = {}, onLearnMoreClick = {},

View file

@ -22,7 +22,7 @@ import io.element.android.features.login.impl.util.openLearnMorePage
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@ -40,7 +40,7 @@ class OnBoardingNode(
fun navigateToQrCode() fun navigateToQrCode()
fun navigateToBugReport() fun navigateToBugReport()
fun navigateToLoginPassword() fun navigateToLoginPassword()
fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToOAuth(oAuthDetails: OAuthDetails)
fun navigateToCreateAccount(url: String) fun navigateToCreateAccount(url: String)
fun navigateToDeveloperSettings() fun navigateToDeveloperSettings()
fun onDone() fun onDone()
@ -71,7 +71,7 @@ class OnBoardingNode(
onCreateAccount = callback::navigateToSignUpFlow, onCreateAccount = callback::navigateToSignUpFlow,
onSignInWithQrCode = callback::navigateToQrCode, onSignInWithQrCode = callback::navigateToQrCode,
onReportProblem = callback::navigateToBugReport, onReportProblem = callback::navigateToBugReport,
onOidcDetails = callback::navigateToOidc, onOAuthDetails = callback::navigateToOAuth,
onNeedLoginPassword = callback::navigateToLoginPassword, onNeedLoginPassword = callback::navigateToLoginPassword,
onLearnMoreClick = { openLearnMorePage(context) }, onLearnMoreClick = { openLearnMorePage(context) },
onCreateAccountContinue = callback::navigateToCreateAccount, onCreateAccountContinue = callback::navigateToCreateAccount,

View file

@ -50,7 +50,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -68,7 +68,7 @@ fun OnBoardingView(
onSignInWithQrCode: () -> Unit, onSignInWithQrCode: () -> Unit,
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
onCreateAccount: () -> Unit, onCreateAccount: () -> Unit,
onOidcDetails: (OidcDetails) -> Unit, onOAuthDetails: (OAuthDetails) -> Unit,
onNeedLoginPassword: () -> Unit, onNeedLoginPassword: () -> Unit,
onLearnMoreClick: () -> Unit, onLearnMoreClick: () -> Unit,
onCreateAccountContinue: (url: String) -> Unit, onCreateAccountContinue: (url: String) -> Unit,
@ -82,7 +82,7 @@ fun OnBoardingView(
state.eventSink(OnBoardingEvents.ClearError) state.eventSink(OnBoardingEvents.ClearError)
}, },
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,
) )
@ -354,7 +354,7 @@ internal fun OnBoardingViewPreview(
onSignIn = {}, onSignIn = {},
onCreateAccount = {}, onCreateAccount = {},
onReportProblem = {}, onReportProblem = {},
onOidcDetails = {}, onOAuthDetails = {},
onNeedLoginPassword = {}, onNeedLoginPassword = {},
onLearnMoreClick = {}, onLearnMoreClick = {},
onCreateAccountContinue = {}, onCreateAccountContinue = {},

View file

@ -17,7 +17,7 @@ import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.classic.FakeElementClassicConnection import io.element.android.features.login.impl.classic.FakeElementClassicConnection
import io.element.android.features.preferences.test.FakePreferencesEntryPoint import io.element.android.features.preferences.test.FakePreferencesEntryPoint
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -39,7 +39,7 @@ class DefaultLoginEntryPointTest {
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
oidcActionFlow = FakeOidcActionFlow(), oAuthActionFlow = FakeOAuthActionFlow(),
appCoroutineScope = backgroundScope, appCoroutineScope = backgroundScope,
elementClassicConnection = FakeElementClassicConnection(), elementClassicConnection = FakeElementClassicConnection(),
preferencesEntryPoint = FakePreferencesEntryPoint(), preferencesEntryPoint = FakePreferencesEntryPoint(),

View file

@ -50,7 +50,7 @@ class ChangeServerPresenterTest {
fun `present - change server ok`() = runTest { fun `present - change server ok`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
createPresenter( createPresenter(

View file

@ -79,7 +79,7 @@ class QrCodeLoginFlowNodeTest {
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected)) assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected))
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OidcMetadataInvalid) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OAuthMetadataInvalid)
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError)) assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError))
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown)

View file

@ -16,7 +16,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -84,7 +84,7 @@ class ChooseAccountProviderViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView(
state: ChooseAccountProviderState, state: ChooseAccountProviderState,
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
@ -93,7 +93,7 @@ class ChooseAccountProviderViewTest {
ChooseAccountProviderView( ChooseAccountProviderView(
state = state, state = state,
onBackClick = onBackClick, onBackClick = onBackClick,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,

View file

@ -22,9 +22,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -74,7 +74,7 @@ class ConfirmAccountProviderPresenterTest {
fun `present - continue oidc`() = runTest { fun `present - continue oidc`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
@ -89,21 +89,21 @@ class ConfirmAccountProviderPresenterTest {
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@Test @Test
fun `present - oidc - cancel with failure`() = runTest { fun `present - OAuth - cancel with failure`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOAuthActionFlow = FakeOAuthActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOAuthActionFlow = defaultOAuthActionFlow,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -114,25 +114,25 @@ class ConfirmAccountProviderPresenterTest {
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
authenticationService.givenOidcCancelError(AN_EXCEPTION) authenticationService.givenOAuthCancelError(AN_EXCEPTION)
defaultOidcActionFlow.post(OidcAction.GoBack()) defaultOAuthActionFlow.post(OAuthAction.GoBack())
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@Test @Test
fun `present - oidc - cancel with success`() = runTest { fun `present - OAuth - cancel with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOAuthActionFlow = FakeOAuthActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOAuthActionFlow = defaultOAuthActionFlow,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -143,24 +143,24 @@ class ConfirmAccountProviderPresenterTest {
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack()) defaultOAuthActionFlow.post(OAuthAction.GoBack())
val cancelFinalState = awaitItem() val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
} }
} }
@Test @Test
fun `present - oidc - cancel to unblock`() = runTest { fun `present - OAuth - cancel to unblock`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOAuthActionFlow = FakeOAuthActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOAuthActionFlow = defaultOAuthActionFlow,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -168,23 +168,23 @@ class ConfirmAccountProviderPresenterTest {
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
val cancelFinalState = awaitItem() val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
} }
} }
@Test @Test
fun `present - oidc - success with failure`() = runTest { fun `present - OAuth - success with failure`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOAuthActionFlow = FakeOAuthActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOAuthActionFlow = defaultOAuthActionFlow,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -195,9 +195,9 @@ class ConfirmAccountProviderPresenterTest {
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
authenticationService.givenLoginError(AN_EXCEPTION) authenticationService.givenLoginError(AN_EXCEPTION)
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
val cancelLoadingState = awaitItem() val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
@ -206,16 +206,16 @@ class ConfirmAccountProviderPresenterTest {
} }
@Test @Test
fun `present - oidc - success with success`() = runTest { fun `present - OAuth - success with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOAuthActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOAuthActionFlow = defaultOidcActionFlow,
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -226,8 +226,8 @@ class ConfirmAccountProviderPresenterTest {
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
val successSuccessState = awaitItem() val successSuccessState = awaitItem()
assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
} }
@ -311,10 +311,10 @@ class ConfirmAccountProviderPresenterTest {
} }
@Test @Test
fun `present - confirm account creation with oidc is successful`() = runTest { fun `present - confirm account creation with OAuth is successful`() = runTest {
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
@ -327,16 +327,16 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@Test @Test
fun `present - confirm account creation with oidc and url continues with oidc`() = runTest { fun `present - confirm account creation with OAuth and url continues with OAuth`() = runTest {
val aUrl = "aUrl" val aUrl = "aUrl"
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
}, },
) )
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
@ -350,12 +350,12 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading skipItems(1) // Loading
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java)
assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
} }
} }
@Test @Test
fun `present - confirm account creation without oidc and with url continuing with url`() = runTest { fun `present - confirm account creation without OAuth and with url continuing with url`() = runTest {
val aUrl = "aUrl" val aUrl = "aUrl"
val authenticationService = FakeMatrixAuthenticationService( val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = { setHomeserverResult = {
@ -380,14 +380,14 @@ class ConfirmAccountProviderPresenterTest {
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
) = ConfirmAccountProviderPresenter( ) = ConfirmAccountProviderPresenter(
params = params, params = params,
accountProviderDataSource = accountProviderDataSource, accountProviderDataSource = accountProviderDataSource,
loginHelper = createLoginHelper( loginHelper = createLoginHelper(
authenticationService = matrixAuthenticationService, authenticationService = matrixAuthenticationService,
oidcActionFlow = defaultOidcActionFlow, oAuthActionFlow = defaultOAuthActionFlow,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
), ),
) )

View file

@ -31,8 +31,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
import io.element.android.libraries.matrix.test.A_LOGIN_HINT import io.element.android.libraries.matrix.test.A_LOGIN_HINT
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
@ -312,11 +312,11 @@ private fun createPresenter(
) )
fun createLoginHelper( fun createLoginHelper(
oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
): LoginHelper = LoginHelper( ): LoginHelper = LoginHelper(
oidcActionFlow = oidcActionFlow, oAuthActionFlow = oAuthActionFlow,
authenticationService = authenticationService, authenticationService = authenticationService,
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
) )

View file

@ -19,7 +19,7 @@ import com.google.testing.junit.testparameterinjector.TestParameter
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -224,14 +224,14 @@ class OnboardingViewTest {
@Test @Test
fun `when success Oidc - the expected callback is invoked and the event is received`() { fun `when success Oidc - the expected callback is invoked and the event is received`() {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
val oidcDetails = OidcDetails("aUrl") val oAuthDetails = OAuthDetails("aUrl")
ensureCalledOnceWithParam(oidcDetails) { callback -> ensureCalledOnceWithParam(oAuthDetails) { callback ->
rule.setOnboardingView( rule.setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)), loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)),
eventSink = eventSink, eventSink = eventSink,
), ),
onOidcDetails = callback, onOAuthDetails = callback,
) )
} }
eventSink.assertSingle(OnBoardingEvents.ClearError) eventSink.assertSingle(OnBoardingEvents.ClearError)
@ -240,8 +240,8 @@ class OnboardingViewTest {
@Test @Test
fun `when success AccountCreation - the expected callback is invoked and the event is received`() { fun `when success AccountCreation - the expected callback is invoked and the event is received`() {
val eventSink = EventsRecorder<OnBoardingEvents>() val eventSink = EventsRecorder<OnBoardingEvents>()
val oidcDetails = OidcDetails("aUrl") val oAuthDetails = OAuthDetails("aUrl")
ensureCalledOnceWithParam(oidcDetails.url) { callback -> ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
rule.setOnboardingView( rule.setOnboardingView(
state = anOnBoardingState( state = anOnBoardingState(
loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")),
@ -261,7 +261,7 @@ class OnboardingViewTest {
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
onCreateAccount: () -> Unit = EnsureNeverCalled(), onCreateAccount: () -> Unit = EnsureNeverCalled(),
onReportProblem: () -> Unit = EnsureNeverCalled(), onReportProblem: () -> Unit = EnsureNeverCalled(),
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
@ -275,7 +275,7 @@ class OnboardingViewTest {
onSignIn = onSignIn, onSignIn = onSignIn,
onCreateAccount = onCreateAccount, onCreateAccount = onCreateAccount,
onReportProblem = onReportProblem, onReportProblem = onReportProblem,
onOidcDetails = onOidcDetails, onOAuthDetails = onOAuthDetails,
onNeedLoginPassword = onNeedLoginPassword, onNeedLoginPassword = onNeedLoginPassword,
onLearnMoreClick = onLearnMoreClick, onLearnMoreClick = onLearnMoreClick,
onCreateAccountContinue = onCreateAccountContinue, onCreateAccountContinue = onCreateAccountContinue,

View file

@ -156,6 +156,7 @@ fun ActionListView(
sheetState = sheetState, sheetState = sheetState,
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
modifier = modifier, modifier = modifier,
scrollable = false,
) { ) {
ActionListViewContent( ActionListViewContent(
state = state, state = state,

View file

@ -75,6 +75,7 @@ fun ResolveVerifiedUserSendFailureView(
.navigationBarsPadding(), .navigationBarsPadding(),
sheetState = sheetState, sheetState = sheetState,
onDismissRequest = ::dismiss, onDismissRequest = ::dismiss,
scrollable = true,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = Modifier.padding(24.dp), modifier = Modifier.padding(24.dp),

View file

@ -13,6 +13,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -74,7 +76,8 @@ internal fun AttachmentsBottomSheet(
sheetState = rememberModalBottomSheetState( sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
), ),
onDismissRequest = { isVisible = false } onDismissRequest = { isVisible = false },
scrollable = false,
) { ) {
AttachmentSourcePickerMenu( AttachmentSourcePickerMenu(
state = state, state = state,
@ -97,6 +100,7 @@ private fun AttachmentSourcePickerMenu(
modifier = Modifier modifier = Modifier
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
.verticalScroll(rememberScrollState())
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.PhotoFromCamera) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.PhotoFromCamera) },

View file

@ -50,7 +50,8 @@ fun CustomReactionBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
sheetState = sheetState, sheetState = sheetState,
modifier = modifier modifier = modifier,
scrollable = false,
) { ) {
val presenter = remember { val presenter = remember {
EmojiPickerPresenter( EmojiPickerPresenter(

View file

@ -90,7 +90,8 @@ fun ReactionSummaryView(
if (state.target != null) { if (state.target != null) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
modifier = modifier modifier = modifier,
scrollable = false,
) { ) {
ReactionSummaryViewContent(summary = state.target) ReactionSummaryViewContent(summary = state.target)
} }

View file

@ -57,7 +57,8 @@ internal fun ReadReceiptBottomSheet(
sheetState.hide() sheetState.hide()
state.eventSink(ReadReceiptBottomSheetEvent.Dismiss) state.eventSink(ReadReceiptBottomSheetEvent.Dismiss)
} }
} },
scrollable = false,
) { ) {
ReadReceiptBottomSheetContent( ReadReceiptBottomSheetContent(
state = state, state = state,

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.indicator.test.FakeIndicatorService import io.element.android.libraries.indicator.test.FakeIndicatorService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID

View file

@ -153,6 +153,7 @@ private fun ChangeOwnRoleBottomSheet(
.navigationBarsPadding(), .navigationBarsPadding(),
sheetState = sheetState, sheetState = sheetState,
onDismissRequest = ::dismiss, onDismissRequest = ::dismiss,
scrollable = true,
) { ) {
Text( Text(
modifier = Modifier.padding(14.dp), modifier = Modifier.padding(14.dp),

View file

@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -224,9 +226,12 @@ private fun RoomMemberActionsBottomSheet(
onDismiss() onDismiss()
} }
}, },
scrollable = false,
) { ) {
Column( Column(
modifier = Modifier.padding(vertical = 16.dp) modifier = Modifier
.padding(vertical = 16.dp)
.verticalScroll(rememberScrollState())
) { ) {
Avatar( Avatar(
avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser), avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser),

View file

@ -34,7 +34,7 @@ dependencies {
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.oidc.api) implementation(projects.libraries.oauth.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
api(libs.statemachine) api(libs.statemachine)

View file

@ -36,7 +36,7 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityOAuthResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -123,12 +123,12 @@ class ResetIdentityFlowNode(
null -> { null -> {
Timber.d("No reset handle return, the reset is done.") Timber.d("No reset handle return, the reset is done.")
} }
is IdentityOidcResetHandle -> { is IdentityOAuthResetHandle -> {
Timber.d("Launching reset confirmation in MAS") Timber.d("Launching reset confirmation in MAS")
activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) activity.openUrlInChromeCustomTab(null, darkTheme, handle.url)
Timber.d("Starting resetOidc") Timber.d("Starting resetOAuth")
resetJob = launch { handle.resetOidc() } resetJob = launch { handle.resetOAuth() }
resetJob?.invokeOnCompletion { Timber.d("resetOidc ended") } resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") }
} }
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
} }

View file

@ -36,7 +36,7 @@ private fun aSessionData(
accessToken = "anAccessToken", accessToken = "anAccessToken",
refreshToken = "aRefreshToken", refreshToken = "aRefreshToken",
homeserverUrl = "aHomeserverUrl", homeserverUrl = "aHomeserverUrl",
oidcData = null, oAuthData = null,
loginTimestamp = null, loginTimestamp = null,
isTokenValid = isTokenValid, isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN, loginType = LoginType.UNKNOWN,

View file

@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -52,11 +54,13 @@ fun JoinRoomByAddressView(
onDismissRequest = { onDismissRequest = {
state.eventSink(JoinRoomByAddressEvent.Dismiss) state.eventSink(JoinRoomByAddressEvent.Dismiss)
}, },
scrollable = false,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp), .padding(all = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
RoomAddressField( RoomAddressField(

View file

@ -51,7 +51,7 @@ telephoto = "0.19.0"
haze = "1.7.2" haze = "1.7.2"
# Dependency analysis # Dependency analysis
dependencyAnalysis = "3.7.0" dependencyAnalysis = "3.9.0"
# DI # DI
metro = "0.13.2" metro = "0.13.2"
@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should # All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs. # only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.21" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.27"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@ -191,7 +191,7 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0" kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0"
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
jsoup = "org.jsoup:jsoup:1.21.2" jsoup = "org.jsoup:jsoup:1.22.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
@ -222,7 +222,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.39.0" posthog = "com.posthog:posthog-android:3.39.0"
sentry = "io.sentry:sentry-android:8.39.1" sentry = "io.sentry:sentry-android:8.40.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2"
@ -234,7 +234,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
# Element Call # Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.19.1" element_call_embedded = "io.element.android:element-call-embedded:0.19.2"
# Auto services # Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }

View file

@ -71,7 +71,7 @@ fun FlowStepPage(
}, },
header = { header = {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp), modifier = Modifier.padding(bottom = 16.dp, start = 8.dp, end = 8.dp),
title = title, title = title,
subTitle = subTitle, subTitle = subTitle,
iconStyle = iconStyle, iconStyle = iconStyle,

View file

@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -38,11 +40,13 @@ fun SimpleModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
modifier = modifier, modifier = modifier,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
scrollable = false,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp)
.verticalScroll(rememberScrollState()),
) { ) {
Text( Text(
title, title,

View file

@ -10,10 +10,13 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -42,10 +45,15 @@ import io.element.android.libraries.designsystem.preview.sheetStateForPreview
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/**
* For parameter [scrollable], set it to true if the content of the sheet does not already contain a scrollable component, such as a LazyColumn,
* to avoid nested scroll issues. In this case, the content will be wrapped in a Column with verticalScroll.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ModalBottomSheet( fun ModalBottomSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
scrollable: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(), sheetState: SheetState = rememberModalBottomSheetState(),
shape: Shape = BottomSheetDefaults.ExpandedShape, shape: Shape = BottomSheetDefaults.ExpandedShape,
@ -79,8 +87,17 @@ fun ModalBottomSheet(
scrimColor = scrimColor, scrimColor = scrimColor,
dragHandle = dragHandle, dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets, contentWindowInsets = contentWindowInsets,
content = content, ) {
) if (scrollable) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
content()
}
} else {
content()
}
}
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -91,13 +108,11 @@ fun SheetState.hide(coroutineScope: CoroutineScope, then: suspend () -> Unit) {
} }
} }
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
@Preview(group = PreviewGroup.BottomSheets) @Preview(group = PreviewGroup.BottomSheets)
@Composable @Composable
internal fun ModalBottomSheetLightPreview() = internal fun ModalBottomSheetLightPreview() =
ElementPreviewLight { ContentToPreview() } ElementPreviewLight { ContentToPreview() }
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
@Preview(group = PreviewGroup.BottomSheets) @Preview(group = PreviewGroup.BottomSheets)
@Composable @Composable
internal fun ModalBottomSheetDarkPreview() = internal fun ModalBottomSheetDarkPreview() =
@ -112,6 +127,7 @@ private fun ContentToPreview() {
) { ) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = {}, onDismissRequest = {},
scrollable = false,
) { ) {
Text( Text(
text = "Sheet Content", text = "Sheet Content",

View file

@ -77,26 +77,28 @@ class DefaultPinnedMessagesBannerFormatter(
messageType.toPlainText(permalinkParser) messageType.toPlainText(permalinkParser)
} }
is VideoMessageType -> { is VideoMessageType -> {
messageType.bestDescription.prefixWith(CommonStrings.common_video) messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_video)
} }
is ImageMessageType -> { is ImageMessageType -> {
messageType.bestDescription.prefixWith(CommonStrings.common_image) messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_image)
} }
is StickerMessageType -> { is StickerMessageType -> {
messageType.bestDescription.prefixWith(CommonStrings.common_sticker) messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_sticker)
} }
is LocationMessageType -> { is LocationMessageType -> {
messageType.body.prefixWith(CommonStrings.common_shared_location) messageType.body.prefixWith(CommonStrings.common_shared_location)
} }
is FileMessageType -> { is FileMessageType -> {
messageType.bestDescription.prefixWith(CommonStrings.common_file) messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_file)
} }
is AudioMessageType -> { is AudioMessageType -> {
messageType.bestDescription.prefixWith(CommonStrings.common_audio) messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_audio)
} }
is VoiceMessageType -> { is VoiceMessageType -> {
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available. messageType
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message)) .toPlainText(permalinkParser, "")
.takeIf { it.isNotEmpty() }
?.prefixWith(sp.getString(CommonStrings.common_voice_message))
?: sp.getString(CommonStrings.common_voice_message) ?: sp.getString(CommonStrings.common_voice_message)
} }
is OtherMessageType -> { is OtherMessageType -> {

View file

@ -139,26 +139,28 @@ class DefaultRoomLatestEventFormatter(
messageType.toPlainText(permalinkParser) messageType.toPlainText(permalinkParser)
} }
is VideoMessageType -> { is VideoMessageType -> {
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_video)) messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_video))
} }
is ImageMessageType -> { is ImageMessageType -> {
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_image)) messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_image))
} }
is StickerMessageType -> { is StickerMessageType -> {
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker)) messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_sticker))
} }
is LocationMessageType -> { is LocationMessageType -> {
sp.getString(CommonStrings.common_shared_location) sp.getString(CommonStrings.common_shared_location)
} }
is FileMessageType -> { is FileMessageType -> {
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_file)) messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_file))
} }
is AudioMessageType -> { is AudioMessageType -> {
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_audio)) messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_audio))
} }
is VoiceMessageType -> { is VoiceMessageType -> {
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available. messageType
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message)) .toPlainText(permalinkParser, "")
.takeIf { it.isNotEmpty() }
?.prefixWith(sp.getString(CommonStrings.common_voice_message))
?: sp.getString(CommonStrings.common_voice_message) ?: sp.getString(CommonStrings.common_voice_message)
} }
is OtherMessageType -> { is OtherMessageType -> {

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom

View file

@ -16,6 +16,6 @@ sealed class AuthenticationException(message: String?) : Exception(message) {
class InvalidServerName(message: String?) : AuthenticationException(message) class InvalidServerName(message: String?) : AuthenticationException(message)
class SlidingSyncVersion(message: String?) : AuthenticationException(message) class SlidingSyncVersion(message: String?) : AuthenticationException(message)
class ServerUnreachable(message: String?) : AuthenticationException(message) class ServerUnreachable(message: String?) : AuthenticationException(message)
class Oidc(message: String?) : AuthenticationException(message) class OAuth(message: String?) : AuthenticationException(message)
class Generic(message: String?) : AuthenticationException(message) class Generic(message: String?) : AuthenticationException(message)
} }

View file

@ -37,21 +37,21 @@ interface MatrixAuthenticationService {
suspend fun importCreatedSession(externalSession: ExternalSession): Result<SessionId> suspend fun importCreatedSession(externalSession: ExternalSession): Result<SessionId>
/* /*
* OIDC part. * OAuth part.
*/ */
/** /**
* Get the Oidc url to display to the user. * Get the OAuth url to display to the user.
*/ */
suspend fun getOidcUrl( suspend fun getOAuthUrl(
prompt: OidcPrompt, prompt: OAuthPrompt,
loginHint: String?, loginHint: String?,
): Result<OidcDetails> ): Result<OAuthDetails>
/** /**
* Cancel Oidc login sequence. * Cancel OAuth login sequence.
*/ */
suspend fun cancelOidcLogin(): Result<Unit> suspend fun cancelOAuthLogin(): Result<Unit>
/** /**
* Set the existing data about Element Classic session, if any. * Set the existing data about Element Classic session, if any.
@ -68,9 +68,9 @@ interface MatrixAuthenticationService {
): Boolean ): Boolean
/** /**
* Attempt to login using the [callbackUrl] provided by the Oidc page. * Attempt to log in using the [callbackUrl] provided by the OAuth page.
*/ */
suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> suspend fun loginWithOAuth(callbackUrl: String): Result<SessionId>
suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId> suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId>

View file

@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.api.auth
data class MatrixHomeServerDetails( data class MatrixHomeServerDetails(
val url: String, val url: String,
val supportsPasswordLogin: Boolean, val supportsPasswordLogin: Boolean,
val supportsOidcLogin: Boolean, val supportsOAuthLogin: Boolean,
) { ) {
val isSupported = supportsPasswordLogin || supportsOidcLogin val isSupported = supportsPasswordLogin || supportsOAuthLogin
} }

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.api.auth
import io.element.android.libraries.matrix.api.BuildConfig import io.element.android.libraries.matrix.api.BuildConfig
object OidcConfig { object OAuthConfig {
const val CLIENT_URI = BuildConfig.CLIENT_URI const val CLIENT_URI = BuildConfig.CLIENT_URI
// Note: host must match with the host of CLIENT_URI // Note: host must match with the host of CLIENT_URI

View file

@ -12,6 +12,6 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class OidcDetails( data class OAuthDetails(
val url: String, val url: String,
) : Parcelable ) : Parcelable

View file

@ -8,12 +8,12 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
sealed interface OidcPrompt { sealed interface OAuthPrompt {
/** /**
* The Authorization Server should prompt the End-User for * The Authorization Server should prompt the End-User for
* reauthentication. * reauthentication.
*/ */
data object Login : OidcPrompt data object Login : OAuthPrompt
/** /**
* The Authorization Server should prompt the End-User to create a user * The Authorization Server should prompt the End-User to create a user
@ -21,10 +21,10 @@ sealed interface OidcPrompt {
* *
* Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html). * Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
*/ */
data object Create : OidcPrompt data object Create : OAuthPrompt
/** /**
* An unknown value. * An unknown value.
*/ */
data class Unknown(val value: String) : OidcPrompt data class Unknown(val value: String) : OAuthPrompt
} }

View file

@ -8,6 +8,6 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
interface OidcRedirectUrlProvider { interface OAuthRedirectUrlProvider {
fun provide(): String fun provide(): String
} }

View file

@ -15,7 +15,7 @@ sealed class QrLoginException : Exception() {
data object Expired : QrLoginException() data object Expired : QrLoginException()
data object NotFound : QrLoginException() data object NotFound : QrLoginException()
data object LinkingNotSupported : QrLoginException() data object LinkingNotSupported : QrLoginException()
data object OidcMetadataInvalid : QrLoginException() data object OAuthMetadataInvalid : QrLoginException()
data object SlidingSyncNotAvailable : QrLoginException() data object SlidingSyncNotAvailable : QrLoginException()
data object OtherDeviceNotSignedIn : QrLoginException() data object OtherDeviceNotSignedIn : QrLoginException()
data object CheckCodeAlreadySent : QrLoginException() data object CheckCodeAlreadySent : QrLoginException()

View file

@ -112,19 +112,19 @@ interface IdentityPasswordResetHandle : IdentityResetHandle {
} }
/** /**
* A handle to reset the user's identity with an OIDC login type. * A handle to reset the user's identity with an OAuth login type.
*/ */
interface IdentityOidcResetHandle : IdentityResetHandle { interface IdentityOAuthResetHandle : IdentityResetHandle {
/** /**
* The URL to open in a webview/custom tab to reset the identity. * The URL to open in a webview/custom tab to reset the identity.
*/ */
val url: String val url: String
/** /**
* Reset the identity using the OIDC flow. * Reset the identity using the OAuth flow.
* *
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
* called, or the identity is reset. * called, or the identity is reset.
*/ */
suspend fun resetOidc(): Result<Unit> suspend fun resetOAuth(): Result<Unit>
} }

View file

@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.libraries.matrix.api.oidc package io.element.android.libraries.matrix.api.oauth
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId

View file

@ -196,9 +196,9 @@ interface JoinedRoom : BaseRoom {
/** /**
* Start sharing live location in this room. * Start sharing live location in this room.
* @param durationMillis How long to share location (in milliseconds). * @param durationMillis How long to share location (in milliseconds).
* @return Result indicating success or failure. * @return Result containing the [EventId] of the beacon state event on success or an error on failure.
*/ */
suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> suspend fun startLiveLocationShare(durationMillis: Long): Result<EventId>
/** /**
* Stop sharing live location in this room. * Stop sharing live location in this room.

View file

@ -16,7 +16,7 @@ class MatrixHomeServerDetailsTest {
@Test @Test
fun `if homeserver supports oidc, then it is supported`() { fun `if homeserver supports oidc, then it is supported`() {
val sut = aMatrixHomeServerDetails( val sut = aMatrixHomeServerDetails(
supportsOidcLogin = true, supportsOAuthLogin = true,
supportsPasswordLogin = false, supportsPasswordLogin = false,
) )
assertThat(sut.isSupported).isTrue() assertThat(sut.isSupported).isTrue()
@ -25,7 +25,7 @@ class MatrixHomeServerDetailsTest {
@Test @Test
fun `if homeserver supports password, then it is supported`() { fun `if homeserver supports password, then it is supported`() {
val sut = aMatrixHomeServerDetails( val sut = aMatrixHomeServerDetails(
supportsOidcLogin = false, supportsOAuthLogin = false,
supportsPasswordLogin = true, supportsPasswordLogin = true,
) )
assertThat(sut.isSupported).isTrue() assertThat(sut.isSupported).isTrue()
@ -34,7 +34,7 @@ class MatrixHomeServerDetailsTest {
@Test @Test
fun `if homeserver supports both, then it is supported`() { fun `if homeserver supports both, then it is supported`() {
val sut = aMatrixHomeServerDetails( val sut = aMatrixHomeServerDetails(
supportsOidcLogin = true, supportsOAuthLogin = true,
supportsPasswordLogin = true, supportsPasswordLogin = true,
) )
assertThat(sut.isSupported).isTrue() assertThat(sut.isSupported).isTrue()
@ -43,7 +43,7 @@ class MatrixHomeServerDetailsTest {
@Test @Test
fun `if homeserver supports none, then it is not supported`() { fun `if homeserver supports none, then it is not supported`() {
val sut = aMatrixHomeServerDetails( val sut = aMatrixHomeServerDetails(
supportsOidcLogin = false, supportsOAuthLogin = false,
supportsPasswordLogin = false, supportsPasswordLogin = false,
) )
assertThat(sut.isSupported).isFalse() assertThat(sut.isSupported).isFalse()

View file

@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
@ -59,7 +59,7 @@ import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.oauth.toRustAction
import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.GetRoomResult import io.element.android.libraries.matrix.impl.room.GetRoomResult
import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom

View file

@ -214,5 +214,5 @@ fun SessionData.toSession() = Session(
deviceId = deviceId, deviceId = deviceId,
homeserverUrl = homeserverUrl, homeserverUrl = homeserverUrl,
slidingSyncVersion = SlidingSyncVersion.NATIVE, slidingSyncVersion = SlidingSyncVersion.NATIVE,
oidcData = oidcData, oauthData = oAuthData,
) )

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException import org.matrix.rustcomponents.sdk.OAuthException
fun Throwable.mapAuthenticationException(): AuthenticationException { fun Throwable.mapAuthenticationException(): AuthenticationException {
return when (this) { return when (this) {
@ -29,12 +29,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
is ClientBuildException.EventCache -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
} }
is OidcException -> when (this) { is OAuthException -> when (this) {
is OidcException.Generic -> AuthenticationException.Oidc(message) is OAuthException.Generic -> AuthenticationException.OAuth(message)
is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message) is OAuthException.CallbackUrlInvalid -> AuthenticationException.OAuth(message)
is OidcException.Cancelled -> AuthenticationException.Oidc(message) is OAuthException.Cancelled -> AuthenticationException.OAuth(message)
is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message) is OAuthException.MetadataInvalid -> AuthenticationException.OAuth(message)
is OidcException.NotSupported -> AuthenticationException.Oidc(message) is OAuthException.NotSupported -> AuthenticationException.OAuth(message)
} }
else -> AuthenticationException.Generic(message) else -> AuthenticationException.Generic(message)
} }

View file

@ -15,6 +15,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
MatrixHomeServerDetails( MatrixHomeServerDetails(
url = url(), url = url(),
supportsPasswordLogin = supportsPasswordLogin(), supportsPasswordLogin = supportsPasswordLogin(),
supportsOidcLogin = supportsOidcLogin(), supportsOAuthLogin = supportsOauthLogin(),
) )
} }

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.OAuthConfig
import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider
import org.matrix.rustcomponents.sdk.OAuthConfiguration
@Inject
class OAuthConfigurationProvider(
private val buildMeta: BuildMeta,
private val oAuthRedirectUrlProvider: OAuthRedirectUrlProvider,
) {
fun get(): OAuthConfiguration = OAuthConfiguration(
clientName = buildMeta.applicationName,
redirectUri = oAuthRedirectUrlProvider.provide(),
clientUri = OAuthConfig.CLIENT_URI,
logoUri = OAuthConfig.LOGO_URI,
tosUri = OAuthConfig.TOS_URI,
policyUri = OAuthConfig.POLICY_URI,
staticRegistrations = OAuthConfig.STATIC_REGISTRATIONS,
)
}

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import org.matrix.rustcomponents.sdk.OidcConfiguration
@Inject
class OidcConfigurationProvider(
private val buildMeta: BuildMeta,
private val oidcRedirectUrlProvider: OidcRedirectUrlProvider,
) {
fun get(): OidcConfiguration = OidcConfiguration(
clientName = buildMeta.applicationName,
redirectUri = oidcRedirectUrlProvider.provide(),
clientUri = OidcConfig.CLIENT_URI,
logoUri = OidcConfig.LOGO_URI,
tosUri = OidcConfig.TOS_URI,
policyUri = OidcConfig.POLICY_URI,
staticRegistrations = OidcConfig.STATIC_REGISTRATIONS,
)
}

View file

@ -8,13 +8,13 @@
package io.element.android.libraries.matrix.impl.auth package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcPrompt import io.element.android.libraries.matrix.api.auth.OAuthPrompt
import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt import org.matrix.rustcomponents.sdk.OAuthPrompt as RustOAuthPrompt
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { internal fun OAuthPrompt.toRustPrompt(): RustOAuthPrompt {
return when (this) { return when (this) {
OidcPrompt.Login -> RustOidcPrompt.Unknown("consent") OAuthPrompt.Login -> RustOAuthPrompt.Unknown("consent")
OidcPrompt.Create -> RustOidcPrompt.Create OAuthPrompt.Create -> RustOAuthPrompt.Create
is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value) is OAuthPrompt.Unknown -> RustOAuthPrompt.Unknown(value)
} }
} }

View file

@ -31,8 +31,8 @@ class RustHomeServerLoginCompatibilityChecker(
it.homeserverLoginDetails() it.homeserverLoginDetails()
} }
.use { .use {
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") Timber.d("Homeserver $url | OAuth: ${it.supportsOauthLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
it.supportsOidcLogin() || it.supportsPasswordLogin() it.supportsOauthLogin() || it.supportsPasswordLogin()
} }
} }
} }

View file

@ -19,8 +19,8 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.ElementClassicSession
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OAuthDetails
import io.element.android.libraries.matrix.api.auth.OidcPrompt import io.element.android.libraries.matrix.api.auth.OAuthPrompt
import io.element.android.libraries.matrix.api.auth.SessionRestorationException import io.element.android.libraries.matrix.api.auth.SessionRestorationException
import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
@ -65,7 +65,7 @@ class RustMatrixAuthenticationService(
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val rustMatrixClientFactory: RustMatrixClientFactory, private val rustMatrixClientFactory: RustMatrixClientFactory,
private val passphraseGenerator: PassphraseGenerator, private val passphraseGenerator: PassphraseGenerator,
private val oidcConfigurationProvider: OidcConfigurationProvider, private val oAuthConfigurationProvider: OAuthConfigurationProvider,
) : MatrixAuthenticationService { ) : MatrixAuthenticationService {
// Any existing Element Classic session that we want to try to import secrets from during login. // Any existing Element Classic session that we want to try to import secrets from during login.
private var elementClassicSession: ElementClassicSession? = null private var elementClassicSession: ElementClassicSession? = null
@ -253,15 +253,15 @@ class RustMatrixAuthenticationService(
private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null
override suspend fun getOidcUrl( override suspend fun getOAuthUrl(
prompt: OidcPrompt, prompt: OAuthPrompt,
loginHint: String?, loginHint: String?,
): Result<OidcDetails> { ): Result<OAuthDetails> {
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first") val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oAuthAuthorizationData = client.urlForOidc( val oAuthAuthorizationData = client.urlForOauth(
oidcConfiguration = oidcConfigurationProvider.get(), oauthConfiguration = oAuthConfigurationProvider.get(),
prompt = prompt.toRustPrompt(), prompt = prompt.toRustPrompt(),
loginHint = loginHint, loginHint = loginHint,
// If we want to restore a previous session for which we have encryption keys, we can pass the deviceId here. At the moment, we don't // If we want to restore a previous session for which we have encryption keys, we can pass the deviceId here. At the moment, we don't
@ -270,23 +270,23 @@ class RustMatrixAuthenticationService(
) )
val url = oAuthAuthorizationData.loginUrl() val url = oAuthAuthorizationData.loginUrl()
pendingOAuthAuthorizationData = oAuthAuthorizationData pendingOAuthAuthorizationData = oAuthAuthorizationData
OidcDetails(url) OAuthDetails(url)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to get OIDC URL") Timber.e(failure, "Failed to get OAuth URL")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
} }
override suspend fun cancelOidcLogin(): Result<Unit> { override suspend fun cancelOAuthLogin(): Result<Unit> {
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
pendingOAuthAuthorizationData?.use { pendingOAuthAuthorizationData?.use {
currentClient?.abortOidcAuth(it) currentClient?.abortOauthAuth(it)
} }
pendingOAuthAuthorizationData = null pendingOAuthAuthorizationData = null
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to cancel OIDC login") Timber.e(failure, "Failed to cancel OAuth login")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -297,14 +297,14 @@ class RustMatrixAuthenticationService(
} }
/** /**
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). * callbackUrl should be the `url` from `OAuthAction` (with all the parameters).
*/ */
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> { override suspend fun loginWithOAuth(callbackUrl: String): Result<SessionId> {
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first") val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.loginWithOidcCallback( client.loginWithOauthCallback(
callbackUrl = callbackUrl, callbackUrl = callbackUrl,
) )
// Free the pending data since we won't use it to abort the flow anymore // Free the pending data since we won't use it to abort the flow anymore
@ -330,7 +330,7 @@ class RustMatrixAuthenticationService(
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to login with OIDC") Timber.e(failure, "Failed to login with OAuth")
failure.mapAuthenticationException() failure.mapAuthenticationException()
} }
} }
@ -355,7 +355,7 @@ class RustMatrixAuthenticationService(
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
val emptySessionPaths = rotateSessionPath() val emptySessionPaths = rotateSessionPath()
val oidcConfiguration = oidcConfigurationProvider.get() val oAuthConfiguration = oAuthConfigurationProvider.get()
val progressListener = object : QrLoginProgressListener { val progressListener = object : QrLoginProgressListener {
override fun onUpdate(state: QrLoginProgress) { override fun onUpdate(state: QrLoginProgress) {
Timber.d("QR Code login progress: $state") Timber.d("QR Code login progress: $state")
@ -368,7 +368,7 @@ class RustMatrixAuthenticationService(
qrCodeData = sdkQrCodeLoginData, qrCodeData = sdkQrCodeLoginData,
) )
client.newLoginWithQrCodeHandler( client.newLoginWithQrCodeHandler(
oidcConfiguration = oidcConfiguration, oauthConfiguration = oAuthConfiguration,
).use { ).use {
it.scan( it.scan(
qrCodeData = qrCodeData.rustQrCodeData, qrCodeData = qrCodeData.rustQrCodeData,

View file

@ -42,7 +42,7 @@ object QrErrorMapper {
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid is RustHumanQrLoginException.OAuthMetadataInvalid -> QrLoginException.OAuthMetadataInvalid
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityOAuthResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import org.matrix.rustcomponents.sdk.AuthData import org.matrix.rustcomponents.sdk.AuthData
@ -25,7 +25,7 @@ object RustIdentityResetHandleFactory {
return runCatchingExceptions { return runCatchingExceptions {
identityResetHandle?.let { identityResetHandle?.let {
when (val authType = identityResetHandle.authType()) { when (val authType = identityResetHandle.authType()) {
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) is CrossSigningResetAuthType.OAuth -> RustIdentityOAuthResetHandle(identityResetHandle, authType.info.approvalUrl)
// User interactive authentication (user + password) // User interactive authentication (user + password)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
} }
@ -47,11 +47,11 @@ class RustPasswordIdentityResetHandle(
} }
} }
class RustOidcIdentityResetHandle( class RustIdentityOAuthResetHandle(
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
override val url: String, override val url: String,
) : IdentityOidcResetHandle { ) : IdentityOAuthResetHandle {
override suspend fun resetOidc(): Result<Unit> { override suspend fun resetOAuth(): Result<Unit> {
return runCatchingExceptions { identityResetHandle.reset(null) } return runCatchingExceptions { identityResetHandle.reset(null) }
} }

View file

@ -54,6 +54,8 @@ class RustLinkDesktopHandler(
} }
} }
) )
// We emit Done in case the progress listener was deallocated before scan() sent the Done
_linkDesktopStep.emit(LinkDesktopStep.Done)
} catch (e: QrCodeDecodeException) { } catch (e: QrCodeDecodeException) {
Timber.tag(tag.value).w(e, "Invalid QR code scanned") Timber.tag(tag.value).w(e, "Invalid QR code scanned")
_linkDesktopStep.emit( _linkDesktopStep.emit(

View file

@ -49,6 +49,8 @@ class RustLinkMobileHandler(
} }
} }
) )
// We emit Done in case the progress listener was deallocated before generate() sent the Done
_linkMobileStep.emit(LinkMobileStep.Done)
} catch (e: HumanQrGrantLoginException) { } catch (e: HumanQrGrantLoginException) {
Timber.tag(tag.value).w(e, "Error during QR login grant") Timber.tag(tag.value).w(e, "Error during QR login grant")
_linkMobileStep.emit(LinkMobileStep.Error(e.map())) _linkMobileStep.emit(LinkMobileStep.Error(e.map()))

View file

@ -27,7 +27,7 @@ internal fun Session.toSessionData(
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken, refreshToken = refreshToken,
homeserverUrl = homeserverUrl ?: this.homeserverUrl, homeserverUrl = homeserverUrl ?: this.homeserverUrl,
oidcData = oidcData, oAuthData = oauthData,
loginTimestamp = Date(), loginTimestamp = Date(),
isTokenValid = isTokenValid, isTokenValid = isTokenValid,
loginType = loginType, loginType = loginType,
@ -52,7 +52,7 @@ internal fun ExternalSession.toSessionData(
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken, refreshToken = refreshToken,
homeserverUrl = homeserverUrl, homeserverUrl = homeserverUrl,
oidcData = null, oAuthData = null,
loginTimestamp = Date(), loginTimestamp = Date(),
isTokenValid = isTokenValid, isTokenValid = isTokenValid,
loginType = loginType, loginType = loginType,

View file

@ -6,9 +6,9 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.libraries.matrix.impl.oidc package io.element.android.libraries.matrix.impl.oauth
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction
fun AccountManagementAction.toRustAction(): RustAccountManagementAction { fun AccountManagementAction.toRustAction(): RustAccountManagementAction {

Some files were not shown because too many files have changed in this diff Show more