Merge branch 'develop' into feature/bma/removeExternalCallSupport
This commit is contained in:
commit
f4b3ddfa0b
262 changed files with 1644 additions and 825 deletions
1
.github/workflows/recordScreenshots.yml
vendored
1
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -17,6 +17,7 @@ jobs:
|
|||
permissions:
|
||||
# Need write permissions on PRs to remove the label "Record-Screenshots"
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'
|
||||
|
|
|
|||
|
|
@ -103,13 +103,13 @@ android {
|
|||
logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]")
|
||||
|
||||
buildTypes {
|
||||
val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
|
||||
val oAuthRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android"
|
||||
getByName("debug") {
|
||||
resValue("string", "app_name", "$baseAppName dbg")
|
||||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
"$oidcRedirectSchemeBase.debug",
|
||||
"$oAuthRedirectSchemeBase.debug",
|
||||
)
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
|
@ -120,7 +120,7 @@ android {
|
|||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
oidcRedirectSchemeBase,
|
||||
oAuthRedirectSchemeBase,
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ android {
|
|||
resValue(
|
||||
"string",
|
||||
"login_redirect_scheme",
|
||||
"$oidcRedirectSchemeBase.nightly",
|
||||
"$oAuthRedirectSchemeBase.nightly",
|
||||
)
|
||||
matchingFallbacks += listOf("release")
|
||||
signingConfig = signingConfigs.getByName("nightly")
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
android:scheme="elementx" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
Oidc redirection
|
||||
OAuth redirection
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ package io.element.android.x.oidc
|
|||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
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.x.R
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOidcRedirectUrlProvider(
|
||||
class DefaultOAuthRedirectUrlProvider(
|
||||
private val stringProvider: StringProvider,
|
||||
) : OidcRedirectUrlProvider {
|
||||
) : OAuthRedirectUrlProvider {
|
||||
override fun provide() = buildString {
|
||||
append(stringProvider.getString(R.string.login_redirect_scheme))
|
||||
append(":/")
|
||||
|
|
@ -13,13 +13,13 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
|||
import io.element.android.x.R
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultOidcRedirectUrlProviderTest {
|
||||
class DefaultOAuthRedirectUrlProviderTest {
|
||||
@Test
|
||||
fun `test provide`() {
|
||||
val stringProvider = FakeStringProvider(
|
||||
defaultResult = "str"
|
||||
)
|
||||
val sut = DefaultOidcRedirectUrlProvider(
|
||||
val sut = DefaultOAuthRedirectUrlProvider(
|
||||
stringProvider = stringProvider,
|
||||
)
|
||||
val result = sut.provide()
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ dependencies {
|
|||
implementation(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
|
|
@ -59,7 +59,7 @@ dependencies {
|
|||
testImplementation(projects.features.login.test)
|
||||
testImplementation(projects.features.share.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
|
|
|
|||
|
|
@ -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.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||
|
|
@ -95,7 +95,7 @@ class RootFlowNode(
|
|||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val accountSelectEntryPoint: AccountSelectEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val announcementService: AnnouncementService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
|
|
@ -392,7 +392,7 @@ class RootFlowNode(
|
|||
navigateTo(resolvedIntent.deeplinkData)
|
||||
}
|
||||
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.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData)
|
||||
}
|
||||
|
|
@ -529,8 +529,8 @@ class RootFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onOidcAction(oidcAction: OidcAction) {
|
||||
oidcActionFlow.post(oidcAction)
|
||||
private fun onOAuthAction(oAuthAction: OAuthAction) {
|
||||
oAuthActionFlow.post(oAuthAction)
|
||||
}
|
||||
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
|
|
|
|||
|
|
@ -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.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcIntentResolver
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthIntentResolver
|
||||
import timber.log.Timber
|
||||
|
||||
sealed interface 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 Login(val params: LoginParams) : ResolvedIntent
|
||||
data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent
|
||||
|
|
@ -34,7 +34,7 @@ sealed interface ResolvedIntent {
|
|||
class IntentResolver(
|
||||
private val deeplinkParser: DeeplinkParser,
|
||||
private val loginIntentResolver: LoginIntentResolver,
|
||||
private val oidcIntentResolver: OidcIntentResolver,
|
||||
private val oAuthIntentResolver: OAuthIntentResolver,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val shareIntentHandler: ShareIntentHandler,
|
||||
) {
|
||||
|
|
@ -45,9 +45,9 @@ class IntentResolver(
|
|||
val deepLinkData = deeplinkParser.getFromIntent(intent)
|
||||
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
|
||||
|
||||
// Coming during login using Oidc?
|
||||
val oidcAction = oidcIntentResolver.resolve(intent)
|
||||
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
|
||||
// Coming during login using OAuth?
|
||||
val oAuthAction = oAuthIntentResolver.resolve(intent)
|
||||
if (oAuthAction != null) return ResolvedIntent.OAuth(oAuthAction)
|
||||
|
||||
val actionViewData = intent
|
||||
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||
|
|
|
|||
|
|
@ -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_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.test.FakeOidcIntentResolver
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -170,9 +170,9 @@ class IntentResolverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `test resolve oidc`() {
|
||||
fun `test resolve OAuth`() {
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { OidcAction.GoBack() },
|
||||
oAuthIntentResolverResult = { OAuthAction.GoBack() },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -180,8 +180,8 @@ class IntentResolverTest {
|
|||
}
|
||||
val result = sut.resolve(intent)
|
||||
assertThat(result).isEqualTo(
|
||||
ResolvedIntent.Oidc(
|
||||
oidcAction = OidcAction.GoBack()
|
||||
ResolvedIntent.OAuth(
|
||||
oAuthAction = OAuthAction.GoBack()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ class IntentResolverTest {
|
|||
val sut = createIntentResolver(
|
||||
loginIntentResolverResult = { null },
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -213,7 +213,7 @@ class IntentResolverTest {
|
|||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
|
||||
loginIntentResolverResult = { null },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -230,7 +230,7 @@ class IntentResolverTest {
|
|||
)
|
||||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { permalinkData },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_BATTERY_LOW
|
||||
|
|
@ -244,7 +244,7 @@ class IntentResolverTest {
|
|||
fun `test incoming share simple`() {
|
||||
val shareIntentData = ShareIntentData.PlainText("Hello")
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
onIncomingShareIntent = { shareIntentData },
|
||||
)
|
||||
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 shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg")))
|
||||
val sut = createIntentResolver(
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
onIncomingShareIntent = { shareIntentData },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
|
|
@ -277,7 +277,7 @@ class IntentResolverTest {
|
|||
val sut = createIntentResolver(
|
||||
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) },
|
||||
loginIntentResolverResult = { null },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -292,7 +292,7 @@ class IntentResolverTest {
|
|||
val aLoginParams = LoginParams("accountProvider", null)
|
||||
val sut = createIntentResolver(
|
||||
loginIntentResolverResult = { aLoginParams },
|
||||
oidcIntentResolverResult = { null },
|
||||
oAuthIntentResolverResult = { null },
|
||||
)
|
||||
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
|
|
@ -306,7 +306,7 @@ class IntentResolverTest {
|
|||
deeplinkParserResult: DeeplinkData? = null,
|
||||
permalinkParserResult: (String) -> PermalinkData = { lambdaError() },
|
||||
loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() },
|
||||
oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() },
|
||||
oAuthIntentResolverResult: (Intent) -> OAuthAction? = { lambdaError() },
|
||||
onIncomingShareIntent: (Intent) -> ShareIntentData? = { null },
|
||||
): IntentResolver {
|
||||
return IntentResolver(
|
||||
|
|
@ -314,8 +314,8 @@ class IntentResolverTest {
|
|||
loginIntentResolver = FakeLoginIntentResolver(
|
||||
parseResult = loginIntentResolverResult,
|
||||
),
|
||||
oidcIntentResolver = FakeOidcIntentResolver(
|
||||
resolveResult = oidcIntentResolverResult,
|
||||
oAuthIntentResolver = FakeOAuthIntentResolver(
|
||||
resolveResult = oAuthIntentResolverResult,
|
||||
),
|
||||
permalinkParser = FakePermalinkParser(
|
||||
result = permalinkParserResult
|
||||
|
|
|
|||
|
|
@ -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.encryption.EncryptionService
|
||||
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.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ tosUri = "https://element.io/user-terms-of-service",
|
|||
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
|
||||
|
||||
Formatted url:
|
||||
|
|
@ -43,8 +43,8 @@ https://auth-oidc.lab.element.dev/authorize?
|
|||
state: ex6mNJVFZ5jn9wL8
|
||||
|
||||
|
||||
Oidc 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 client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
||||
OAuth sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
||||
|
||||
|
||||
Test server:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,11 +64,15 @@ internal fun CallScreenView(
|
|||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun handleBack() {
|
||||
if (pipState.supportPip) {
|
||||
pipState.eventSink.invoke(PictureInPictureEvent.EnterPictureInPicture)
|
||||
} else {
|
||||
state.eventSink(CallScreenEvent.Hangup)
|
||||
var callWebView by remember { mutableStateOf<WebView?>(null) }
|
||||
|
||||
fun handleBack(fromNative: Boolean = false) {
|
||||
when (CallScreenBackPressPolicy.resolve(supportPip = pipState.supportPip, hasWebView = callWebView != null, fromNative)) {
|
||||
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,
|
||||
) { padding ->
|
||||
BackHandler {
|
||||
handleBack()
|
||||
handleBack(fromNative = true)
|
||||
}
|
||||
if (state.webViewError != null) {
|
||||
ErrorDialog(
|
||||
|
|
@ -111,6 +115,7 @@ internal fun CallScreenView(
|
|||
},
|
||||
onConsoleMessage = onConsoleMessage,
|
||||
onCreateWebView = { webView ->
|
||||
callWebView = webView
|
||||
webView.addBackHandler(onBackPressed = ::handleBack)
|
||||
val interceptor = WebViewWidgetMessageInterceptor(
|
||||
webView = webView,
|
||||
|
|
@ -135,6 +140,7 @@ internal fun CallScreenView(
|
|||
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
||||
},
|
||||
onDestroyWebView = {
|
||||
callWebView = null
|
||||
// Reset audio mode
|
||||
webViewAudioManager?.onCallStopped()
|
||||
}
|
||||
|
|
@ -143,6 +149,7 @@ internal fun CallScreenView(
|
|||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading ->
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
|
||||
|
||||
is AsyncData.Failure -> {
|
||||
Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}")
|
||||
ErrorDialog(
|
||||
|
|
@ -150,6 +157,7 @@ internal fun CallScreenView(
|
|||
onSubmit = { state.eventSink(CallScreenEvent.Hangup) },
|
||||
)
|
||||
}
|
||||
|
||||
is AsyncData.Success -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -248,15 +256,18 @@ private fun WebView.setup(
|
|||
|
||||
private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
|
||||
addJavascriptInterface(
|
||||
object {
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun onBackPressed() = onBackPressed()
|
||||
JavascriptBackHandler {
|
||||
onBackPressed()
|
||||
},
|
||||
"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
|
||||
@Composable
|
||||
internal fun CallScreenViewPreview(
|
||||
|
|
@ -275,3 +286,8 @@ internal fun CallScreenViewPreview(
|
|||
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
|
||||
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
|
||||
}
|
||||
|
||||
internal fun interface JavascriptBackHandler {
|
||||
@JavascriptInterface
|
||||
fun onBackPressed()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,8 @@ internal fun SelectParentSpaceOptions(
|
|||
sheetState.hide(coroutineScope) {
|
||||
displaySelectSpaceBottomSheet = false
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
SelectParentSpaceBottomSheet(
|
||||
spaces = spaces,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ package io.element.android.features.home.impl.roomlist
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -43,6 +45,7 @@ fun RoomListContextMenu(
|
|||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { eventSink(RoomListEvent.HideContextMenu) },
|
||||
scrollable = false,
|
||||
) {
|
||||
RoomListModalBottomSheetContent(
|
||||
contextMenu = contextMenu,
|
||||
|
|
@ -91,7 +94,9 @@ private fun RoomListModalBottomSheetContent(
|
|||
onReportRoomClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListItem(
|
||||
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
|
||||
@Composable
|
||||
internal fun RoomListModalBottomSheetContentPreview(
|
||||
internal fun RoomListContextMenuPreview(
|
||||
@PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
|
||||
) = ElementPreview {
|
||||
RoomListModalBottomSheetContent(
|
||||
RoomListContextMenu(
|
||||
contextMenu = contextMenu,
|
||||
canReportRoom = true,
|
||||
onRoomMarkReadClick = {},
|
||||
onRoomMarkUnreadClick = {},
|
||||
onRoomSettingsClick = {},
|
||||
onLeaveRoomClick = {},
|
||||
onFavoriteChange = {},
|
||||
onClearCacheRoomClick = {},
|
||||
onReportRoomClick = {},
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,21 @@ 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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.appconfig.ProtectionConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.home.impl.R
|
||||
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.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
|
|
@ -42,9 +47,14 @@ fun RoomListDeclineInviteMenu(
|
|||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { eventSink(RoomListEvent.HideDeclineInviteMenu) },
|
||||
scrollable = false,
|
||||
) {
|
||||
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 = {
|
||||
eventSink(RoomListEvent.HideDeclineInviteMenu)
|
||||
eventSink(RoomListEvent.DeclineInvite(menu.roomSummary, false))
|
||||
|
|
@ -74,7 +84,8 @@ private fun RoomListDeclineInviteMenuContent(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp),
|
||||
.padding(all = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
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
|
||||
@Composable
|
||||
internal fun RoomListDeclineInviteMenuContentPreview() = ElementPreview {
|
||||
RoomListDeclineInviteMenuContent(
|
||||
roomName = "Room name",
|
||||
onCancelClick = {},
|
||||
onDeclineClick = {},
|
||||
internal fun RoomListDeclineInviteMenuPreview(
|
||||
@PreviewParameter(RoomListStateDeclineInviteMenuShownProvider::class) menu: RoomListState.DeclineInviteMenu.Shown,
|
||||
) = ElementPreview {
|
||||
RoomListDeclineInviteMenu(
|
||||
menu = menu,
|
||||
canReportRoom = false,
|
||||
onDeclineAndBlockClick = {},
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -81,7 +81,8 @@ fun SpaceFiltersView(
|
|||
if (state is SpaceFiltersState.Selecting) {
|
||||
state.eventSink(SpaceFiltersEvent.Selecting.Cancel)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ private fun InvitePeopleConfirmModal(
|
|||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
dragHandle = null,
|
||||
scrollable = false,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
title = simplePluralStringResource(
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ dependencies {
|
|||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(libs.androidx.browser)
|
||||
|
|
@ -56,7 +56,7 @@ dependencies {
|
|||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import dev.zacsweers.metro.AssistedInject
|
|||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
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.error.ErrorNode
|
||||
import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType
|
||||
|
|
@ -107,6 +108,11 @@ class LinkNewDeviceFlowNode(
|
|||
val data: String,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class CodeConfirmation(
|
||||
val code: String,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object MobileEnterNumber : NavTarget
|
||||
|
||||
|
|
@ -145,10 +151,7 @@ class LinkNewDeviceFlowNode(
|
|||
LinkMobileStep.Starting -> {
|
||||
// This step is not received at the moment, so do nothing
|
||||
}
|
||||
LinkMobileStep.SyncingSecrets -> {
|
||||
// LinkMobileStep.Done is not received at the moment, so consider that the flow is done here
|
||||
callback.onDone()
|
||||
}
|
||||
LinkMobileStep.SyncingSecrets -> Unit
|
||||
is LinkMobileStep.WaitingForAuth -> {
|
||||
navigateToBrowser(linkMobileStep.verificationUri)
|
||||
}
|
||||
|
|
@ -166,7 +169,9 @@ class LinkNewDeviceFlowNode(
|
|||
is LinkDesktopStep.Error -> {
|
||||
navigateToError(linkDesktopStep.errorType)
|
||||
}
|
||||
is LinkDesktopStep.EstablishingSecureChannel -> Unit
|
||||
is LinkDesktopStep.EstablishingSecureChannel -> {
|
||||
backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString))
|
||||
}
|
||||
is LinkDesktopStep.InvalidQrCode -> {
|
||||
// This error will be handled by the ScanQrCodeNode
|
||||
}
|
||||
|
|
@ -183,20 +188,20 @@ class LinkNewDeviceFlowNode(
|
|||
|
||||
private fun navigateToError(errorType: ErrorType) {
|
||||
// Map the error to an error screen
|
||||
// TODO Update this mapping
|
||||
val error = when (errorType) {
|
||||
is ErrorType.DeviceIdAlreadyInUse -> ErrorScreenType.UnknownError
|
||||
is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected
|
||||
is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError
|
||||
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.InvalidCheckCode -> ErrorScreenType.Mismatch2Digits
|
||||
is ErrorType.UnsupportedProtocol -> ErrorScreenType.ProtocolNotSupported
|
||||
is ErrorType.Cancelled -> ErrorScreenType.Cancelled
|
||||
is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected
|
||||
is ErrorType.Expired -> ErrorScreenType.Expired
|
||||
is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError
|
||||
is ErrorType.Expired,
|
||||
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.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,
|
||||
// or the whole flow will be popped.
|
||||
|
|
@ -250,6 +255,18 @@ class LinkNewDeviceFlowNode(
|
|||
}
|
||||
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 -> {
|
||||
val callback = object : ShowQrCodeNode.Callback {
|
||||
override fun navigateBack() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,9 @@ sealed interface ErrorScreenType : NodeInputs, Parcelable {
|
|||
@Parcelize
|
||||
data object Expired : ErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object OtherDeviceAlreadySignedIn : ErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object Mismatch2Digits : ErrorScreenType
|
||||
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ class ErrorScreenTypeProvider : PreviewParameterProvider<ErrorScreenType> {
|
|||
ErrorScreenType.InsecureChannelDetected,
|
||||
ErrorScreenType.SlidingSyncNotAvailable,
|
||||
ErrorScreenType.UnknownError,
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,17 +47,26 @@ fun ErrorView(
|
|||
) {
|
||||
val appName = LocalBuildMeta.current.applicationName
|
||||
BackHandler(onBack = onCancel)
|
||||
val iconStyle = when (errorScreenType) {
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> BigIcon.Style.SuccessSolid
|
||||
else -> BigIcon.Style.AlertSolid
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
iconStyle = iconStyle,
|
||||
title = titleText(errorScreenType, appName),
|
||||
subTitle = subtitleText(errorScreenType, appName),
|
||||
content = { Content(errorScreenType) },
|
||||
buttons = {
|
||||
Buttons(
|
||||
onRetry = onRetry,
|
||||
onCancel = onCancel,
|
||||
)
|
||||
when (errorScreenType) {
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> DoneButton(
|
||||
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.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)
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_title)
|
||||
}
|
||||
|
||||
@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.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)
|
||||
ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_subtitle)
|
||||
}
|
||||
|
||||
@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
|
||||
private fun Buttons(
|
||||
onRetry: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -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_3">"If that doesn’t 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_device_code_subtitle">"You’ll 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_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>
|
||||
|
|
@ -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 device’s 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_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>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ dependencies {
|
|||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(libs.androidx.browser)
|
||||
|
|
@ -83,7 +83,7 @@ dependencies {
|
|||
testImplementation(projects.features.preferences.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.oidc.test)
|
||||
testImplementation(projects.libraries.oauth.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.wellknown.test)
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ import io.element.android.libraries.architecture.callback
|
|||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -64,7 +64,7 @@ class LoginFlowNode(
|
|||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val elementClassicConnection: ElementClassicConnection,
|
||||
|
|
@ -100,7 +100,7 @@ class LoginFlowNode(
|
|||
// by pressing back or by closing the Custom Chrome Tab.
|
||||
lifecycleScope.launch {
|
||||
delay(5000)
|
||||
oidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
|
||||
oAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,8 +161,8 @@ class LoginFlowNode(
|
|||
backstack.push(NavTarget.LoginPassword())
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
|
@ -197,8 +197,8 @@ class LoginFlowNode(
|
|||
callback.navigateToBugReport()
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
|
@ -243,8 +243,8 @@ class LoginFlowNode(
|
|||
}
|
||||
NavTarget.ChooseAccountProvider -> {
|
||||
val callback = object : ChooseAccountProviderNode.Callback {
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
|
@ -270,8 +270,8 @@ class LoginFlowNode(
|
|||
isAccountCreation = navTarget.isAccountCreation,
|
||||
)
|
||||
val callback = object : ConfirmAccountProviderNode.Callback {
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
navigateToMas(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
navigateToMas(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
|
@ -333,10 +333,10 @@ class LoginFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
private fun navigateToMas(oidcDetails: OidcDetails) {
|
||||
private fun navigateToMas(oAuthDetails: OAuthDetails) {
|
||||
activity?.let {
|
||||
externalAppStarted = true
|
||||
it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url)
|
||||
it.openUrlInChromeCustomTab(null, darkTheme, oAuthDetails.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ sealed class ChangeServerError : Exception() {
|
|||
// AccountAlreadyLoggedIn error should not happen at this point
|
||||
is AuthenticationException.AccountAlreadyLoggedIn -> 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(
|
||||
|
|
|
|||
|
|
@ -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.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
|
||||
/**
|
||||
* 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
|
||||
class LoginHelper(
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
private val oAuthActionFlow: OAuthActionFlow,
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever,
|
||||
) {
|
||||
|
|
@ -44,9 +44,9 @@ class LoginHelper(
|
|||
@Composable
|
||||
fun collectLoginMode(): State<AsyncData<LoginMode>> {
|
||||
LaunchedEffect(Unit) {
|
||||
oidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction)
|
||||
oAuthActionFlow.collect { oAuthAction ->
|
||||
if (oAuthAction != null) {
|
||||
onOAuthAction(oAuthAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,11 +73,11 @@ class LoginHelper(
|
|||
throw it
|
||||
}
|
||||
}.map { matrixHomeServerDetails ->
|
||||
if (matrixHomeServerDetails.supportsOidcLogin) {
|
||||
if (matrixHomeServerDetails.supportsOAuthLogin) {
|
||||
// Retrieve the details right now
|
||||
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login
|
||||
LoginMode.Oidc(
|
||||
authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow()
|
||||
val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login
|
||||
LoginMode.OAuth(
|
||||
authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow()
|
||||
)
|
||||
} else if (isAccountCreation) {
|
||||
val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl)
|
||||
|
|
@ -99,16 +99,16 @@ class LoginHelper(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun onOidcAction(oidcAction: OidcAction) {
|
||||
if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) {
|
||||
private suspend fun onOAuthAction(oAuthAction: OAuthAction) {
|
||||
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.
|
||||
// This can happen if there is an error, for instance attempt to login again on the same account.
|
||||
return
|
||||
}
|
||||
loginModeState.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
is OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
when (oAuthAction) {
|
||||
is OAuthAction.GoBack -> {
|
||||
authenticationService.cancelOAuthLogin()
|
||||
.onSuccess {
|
||||
loginModeState.value = AsyncData.Uninitialized
|
||||
}
|
||||
|
|
@ -116,13 +116,13 @@ class LoginHelper(
|
|||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
authenticationService.loginWithOidc(oidcAction.url)
|
||||
is OAuthAction.Success -> {
|
||||
authenticationService.loginWithOAuth(oAuthAction.url)
|
||||
.onFailure { failure ->
|
||||
loginModeState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
oidcActionFlow.reset()
|
||||
oAuthActionFlow.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.theme.LocalBuildMeta
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -32,7 +32,7 @@ fun LoginModeView(
|
|||
loginMode: AsyncData<LoginMode>,
|
||||
onClearError: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit
|
||||
) {
|
||||
|
|
@ -118,7 +118,7 @@ fun LoginModeView(
|
|||
is AsyncData.Loading -> Unit // The Continue button shows the loading state
|
||||
is AsyncData.Success -> {
|
||||
when (val loginModeData = loginMode.data) {
|
||||
is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails)
|
||||
is LoginMode.OAuth -> onOAuthDetails(loginModeData.oAuthDetails)
|
||||
LoginMode.PasswordLogin -> onNeedLoginPassword()
|
||||
is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url)
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::
|
|||
loginMode = AsyncData.Failure(error),
|
||||
onClearError = {},
|
||||
onLearnMoreClick = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onCreateAccountContinue = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ class QrCodeLoginFlowNode(
|
|||
is QrLoginException.SlidingSyncNotAvailable -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable))
|
||||
}
|
||||
is QrLoginException.OidcMetadataInvalid -> {
|
||||
Timber.e(error, "OIDC metadata is invalid")
|
||||
is QrLoginException.OAuthMetadataInvalid -> {
|
||||
Timber.e(error, "OAuth metadata is invalid")
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
}
|
||||
QrLoginException.CheckCodeAlreadySent,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import dev.zacsweers.metro.AssistedInject
|
|||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.login.impl.util.openLearnMorePage
|
||||
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)
|
||||
@AssistedInject
|
||||
|
|
@ -31,7 +31,7 @@ class ChooseAccountProviderNode(
|
|||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ class ChooseAccountProviderNode(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = ::navigateUp,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
|
|
|||
|
|
@ -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.Scaffold
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun ChooseAccountProviderView(
|
||||
state: ChooseAccountProviderState,
|
||||
onBackClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
|
|
@ -129,7 +129,7 @@ fun ChooseAccountProviderView(
|
|||
state.eventSink(ChooseAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
|
|
@ -144,7 +144,7 @@ internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountPro
|
|||
state = state,
|
||||
onBackClick = { },
|
||||
onLearnMoreClick = { },
|
||||
onOidcDetails = { },
|
||||
onOAuthDetails = { },
|
||||
onNeedLoginPassword = { },
|
||||
onCreateAccountContinue = { },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.callback
|
||||
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 kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -54,7 +54,7 @@ class ClassicFlowNode(
|
|||
interface Callback : Plugin {
|
||||
fun navigateToOnBoarding(allowBackNavigation: Boolean)
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
}
|
||||
|
||||
|
|
@ -111,8 +111,8 @@ class ClassicFlowNode(
|
|||
callback.navigateToLoginPassword()
|
||||
}
|
||||
|
||||
override fun navigateToOidc(oidcDetails: OidcDetails) {
|
||||
callback.navigateToOidc(oidcDetails)
|
||||
override fun navigateToOAuth(oAuthDetails: OAuthDetails) {
|
||||
callback.navigateToOAuth(oAuthDetails)
|
||||
}
|
||||
|
||||
override fun navigateToCreateAccount(url: String) {
|
||||
|
|
|
|||
|
|
@ -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.callback
|
||||
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
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
|
|
@ -35,7 +35,7 @@ class LoginWithClassicNode(
|
|||
interface Callback : Plugin {
|
||||
fun navigateToOtherOptions()
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToMissingKeyBackup()
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ class LoginWithClassicNode(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onOtherOptionsClick = callback::navigateToOtherOptions,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
|
|
|||
|
|
@ -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.OutlinedButton
|
||||
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.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
fun LoginWithClassicView(
|
||||
state: LoginWithClassicState,
|
||||
onOtherOptionsClick: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
|
|
@ -200,7 +200,7 @@ fun LoginWithClassicView(
|
|||
state.eventSink(LoginWithClassicEvent.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
|
|
@ -212,7 +212,7 @@ internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicState
|
|||
LoginWithClassicView(
|
||||
state = state,
|
||||
onOtherOptionsClick = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onLearnMoreClick = {},
|
||||
onCreateAccountContinue = {},
|
||||
|
|
|
|||
|
|
@ -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.callback
|
||||
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)
|
||||
@AssistedInject
|
||||
|
|
@ -44,7 +44,7 @@ class ConfirmAccountProviderNode(
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToChangeAccountProvider()
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ class ConfirmAccountProviderNode(
|
|||
ConfirmAccountProviderView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
onChange = callback::navigateToChangeAccountProvider,
|
||||
|
|
|
|||
|
|
@ -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.theme.components.Button
|
||||
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.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -38,7 +38,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun ConfirmAccountProviderView(
|
||||
state: ConfirmAccountProviderState,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
|
|
@ -103,7 +103,7 @@ fun ConfirmAccountProviderView(
|
|||
eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
|
|
@ -117,7 +117,7 @@ internal fun ConfirmAccountProviderViewPreview(
|
|||
) = ElementPreview {
|
||||
ConfirmAccountProviderView(
|
||||
state = state,
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onCreateAccountContinue = {},
|
||||
onLearnMoreClick = {},
|
||||
|
|
|
|||
|
|
@ -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.callback
|
||||
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)
|
||||
@AssistedInject
|
||||
|
|
@ -40,7 +40,7 @@ class OnBoardingNode(
|
|||
fun navigateToQrCode()
|
||||
fun navigateToBugReport()
|
||||
fun navigateToLoginPassword()
|
||||
fun navigateToOidc(oidcDetails: OidcDetails)
|
||||
fun navigateToOAuth(oAuthDetails: OAuthDetails)
|
||||
fun navigateToCreateAccount(url: String)
|
||||
fun navigateToDeveloperSettings()
|
||||
fun onDone()
|
||||
|
|
@ -71,7 +71,7 @@ class OnBoardingNode(
|
|||
onCreateAccount = callback::navigateToSignUpFlow,
|
||||
onSignInWithQrCode = callback::navigateToQrCode,
|
||||
onReportProblem = callback::navigateToBugReport,
|
||||
onOidcDetails = callback::navigateToOidc,
|
||||
onOAuthDetails = callback::navigateToOAuth,
|
||||
onNeedLoginPassword = callback::navigateToLoginPassword,
|
||||
onLearnMoreClick = { openLearnMorePage(context) },
|
||||
onCreateAccountContinue = callback::navigateToCreateAccount,
|
||||
|
|
|
|||
|
|
@ -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.Text
|
||||
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.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -68,7 +68,7 @@ fun OnBoardingView(
|
|||
onSignInWithQrCode: () -> Unit,
|
||||
onSignIn: (mustChooseAccountProvider: Boolean) -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onOidcDetails: (OidcDetails) -> Unit,
|
||||
onOAuthDetails: (OAuthDetails) -> Unit,
|
||||
onNeedLoginPassword: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCreateAccountContinue: (url: String) -> Unit,
|
||||
|
|
@ -82,7 +82,7 @@ fun OnBoardingView(
|
|||
state.eventSink(OnBoardingEvents.ClearError)
|
||||
},
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
)
|
||||
|
|
@ -354,7 +354,7 @@ internal fun OnBoardingViewPreview(
|
|||
onSignIn = {},
|
||||
onCreateAccount = {},
|
||||
onReportProblem = {},
|
||||
onOidcDetails = {},
|
||||
onOAuthDetails = {},
|
||||
onNeedLoginPassword = {},
|
||||
onLearnMoreClick = {},
|
||||
onCreateAccountContinue = {},
|
||||
|
|
|
|||
|
|
@ -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.classic.FakeElementClassicConnection
|
||||
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.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -39,7 +39,7 @@ class DefaultLoginEntryPointTest {
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
oidcActionFlow = FakeOidcActionFlow(),
|
||||
oAuthActionFlow = FakeOAuthActionFlow(),
|
||||
appCoroutineScope = backgroundScope,
|
||||
elementClassicConnection = FakeElementClassicConnection(),
|
||||
preferencesEntryPoint = FakePreferencesEntryPoint(),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class ChangeServerPresenterTest {
|
|||
fun `present - change server ok`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
createPresenter(
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class QrCodeLoginFlowNodeTest {
|
|||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure)
|
||||
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))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
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.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
|
|
@ -84,7 +84,7 @@ class ChooseAccountProviderViewTest {
|
|||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChooseAccountProviderView(
|
||||
state: ChooseAccountProviderState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
|
|
@ -93,7 +93,7 @@ class ChooseAccountProviderViewTest {
|
|||
ChooseAccountProviderView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
|
|
|
|||
|
|
@ -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.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||
import io.element.android.libraries.oidc.api.OidcAction
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthAction
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -74,7 +74,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
fun `present - continue oidc`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
|
|
@ -89,21 +89,21 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
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
|
||||
fun `present - oidc - cancel with failure`() = runTest {
|
||||
fun `present - OAuth - cancel with failure`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -114,25 +114,25 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
authenticationService.givenOidcCancelError(AN_EXCEPTION)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack())
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
authenticationService.givenOAuthCancelError(AN_EXCEPTION)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFailureState = awaitItem()
|
||||
assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - cancel with success`() = runTest {
|
||||
fun `present - OAuth - cancel with success`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -143,24 +143,24 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack())
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack())
|
||||
val cancelFinalState = awaitItem()
|
||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - cancel to unblock`() = runTest {
|
||||
fun `present - OAuth - cancel to unblock`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -168,23 +168,23 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true))
|
||||
defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true))
|
||||
val cancelFinalState = awaitItem()
|
||||
assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - success with failure`() = runTest {
|
||||
fun `present - OAuth - success with failure`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOAuthActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOAuthActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -195,9 +195,9 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
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)
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
defaultOAuthActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val cancelLoadingState = awaitItem()
|
||||
assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val cancelFailureState = awaitItem()
|
||||
|
|
@ -206,16 +206,16 @@ class ConfirmAccountProviderPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - oidc - success with success`() = runTest {
|
||||
fun `present - OAuth - success with success`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val defaultOidcActionFlow = FakeOidcActionFlow()
|
||||
val defaultOidcActionFlow = FakeOAuthActionFlow()
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
matrixAuthenticationService = authenticationService,
|
||||
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||
defaultOAuthActionFlow = defaultOidcActionFlow,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -226,8 +226,8 @@ class ConfirmAccountProviderPresenterTest {
|
|||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java)
|
||||
defaultOidcActionFlow.post(OAuthAction.Success("aUrl"))
|
||||
val successSuccessState = awaitItem()
|
||||
assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java)
|
||||
}
|
||||
|
|
@ -311,10 +311,10 @@ class ConfirmAccountProviderPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm account creation with oidc is successful`() = runTest {
|
||||
fun `present - confirm account creation with OAuth is successful`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
|
|
@ -327,16 +327,16 @@ class ConfirmAccountProviderPresenterTest {
|
|||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
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
|
||||
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 authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
|
||||
Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true))
|
||||
},
|
||||
)
|
||||
val presenter = createConfirmAccountProviderPresenter(
|
||||
|
|
@ -350,12 +350,12 @@ class ConfirmAccountProviderPresenterTest {
|
|||
skipItems(1) // Loading
|
||||
val submittedState = awaitItem()
|
||||
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
|
||||
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 authenticationService = FakeMatrixAuthenticationService(
|
||||
setHomeserverResult = {
|
||||
|
|
@ -380,14 +380,14 @@ class ConfirmAccountProviderPresenterTest {
|
|||
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
|
||||
defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||
) = ConfirmAccountProviderPresenter(
|
||||
params = params,
|
||||
accountProviderDataSource = accountProviderDataSource,
|
||||
loginHelper = createLoginHelper(
|
||||
authenticationService = matrixAuthenticationService,
|
||||
oidcActionFlow = defaultOidcActionFlow,
|
||||
oAuthActionFlow = defaultOAuthActionFlow,
|
||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
|
|
@ -312,11 +312,11 @@ private fun createPresenter(
|
|||
)
|
||||
|
||||
fun createLoginHelper(
|
||||
oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(),
|
||||
oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(),
|
||||
authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(),
|
||||
): LoginHelper = LoginHelper(
|
||||
oidcActionFlow = oidcActionFlow,
|
||||
oAuthActionFlow = oAuthActionFlow,
|
||||
authenticationService = authenticationService,
|
||||
webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.login.LoginMode
|
||||
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.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
|
|
@ -224,14 +224,14 @@ class OnboardingViewTest {
|
|||
@Test
|
||||
fun `when success Oidc - the expected callback is invoked and the event is received`() {
|
||||
val eventSink = EventsRecorder<OnBoardingEvents>()
|
||||
val oidcDetails = OidcDetails("aUrl")
|
||||
ensureCalledOnceWithParam(oidcDetails) { callback ->
|
||||
val oAuthDetails = OAuthDetails("aUrl")
|
||||
ensureCalledOnceWithParam(oAuthDetails) { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(
|
||||
loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)),
|
||||
loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)),
|
||||
eventSink = eventSink,
|
||||
),
|
||||
onOidcDetails = callback,
|
||||
onOAuthDetails = callback,
|
||||
)
|
||||
}
|
||||
eventSink.assertSingle(OnBoardingEvents.ClearError)
|
||||
|
|
@ -240,8 +240,8 @@ class OnboardingViewTest {
|
|||
@Test
|
||||
fun `when success AccountCreation - the expected callback is invoked and the event is received`() {
|
||||
val eventSink = EventsRecorder<OnBoardingEvents>()
|
||||
val oidcDetails = OidcDetails("aUrl")
|
||||
ensureCalledOnceWithParam(oidcDetails.url) { callback ->
|
||||
val oAuthDetails = OAuthDetails("aUrl")
|
||||
ensureCalledOnceWithParam(oAuthDetails.url) { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(
|
||||
loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")),
|
||||
|
|
@ -261,7 +261,7 @@ class OnboardingViewTest {
|
|||
onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
||||
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
||||
onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onNeedLoginPassword: () -> Unit = EnsureNeverCalled(),
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
|
|
@ -275,7 +275,7 @@ class OnboardingViewTest {
|
|||
onSignIn = onSignIn,
|
||||
onCreateAccount = onCreateAccount,
|
||||
onReportProblem = onReportProblem,
|
||||
onOidcDetails = onOidcDetails,
|
||||
onOAuthDetails = onOAuthDetails,
|
||||
onNeedLoginPassword = onNeedLoginPassword,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onCreateAccountContinue = onCreateAccountContinue,
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ fun ActionListView(
|
|||
sheetState = sheetState,
|
||||
onDismissRequest = ::onDismiss,
|
||||
modifier = modifier,
|
||||
scrollable = false,
|
||||
) {
|
||||
ActionListViewContent(
|
||||
state = state,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ fun ResolveVerifiedUserSendFailureView(
|
|||
.navigationBarsPadding(),
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::dismiss,
|
||||
scrollable = true,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
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.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -74,7 +76,8 @@ internal fun AttachmentsBottomSheet(
|
|||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
onDismissRequest = { isVisible = false }
|
||||
onDismissRequest = { isVisible = false },
|
||||
scrollable = false,
|
||||
) {
|
||||
AttachmentSourcePickerMenu(
|
||||
state = state,
|
||||
|
|
@ -97,6 +100,7 @@ private fun AttachmentSourcePickerMenu(
|
|||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.PhotoFromCamera) },
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ fun CustomReactionBottomSheet(
|
|||
ModalBottomSheet(
|
||||
onDismissRequest = ::onDismiss,
|
||||
sheetState = sheetState,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
scrollable = false,
|
||||
) {
|
||||
val presenter = remember {
|
||||
EmojiPickerPresenter(
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ fun ReactionSummaryView(
|
|||
if (state.target != null) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = ::onDismiss,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
scrollable = false,
|
||||
) {
|
||||
ReactionSummaryViewContent(summary = state.target)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ internal fun ReadReceiptBottomSheet(
|
|||
sheetState.hide()
|
||||
state.eventSink(ReadReceiptBottomSheetEvent.Dismiss)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
ReadReceiptBottomSheetContent(
|
||||
state = state,
|
||||
|
|
|
|||
|
|
@ -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.indicator.api.IndicatorService
|
||||
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.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ private fun ChangeOwnRoleBottomSheet(
|
|||
.navigationBarsPadding(),
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = ::dismiss,
|
||||
scrollable = true,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.heightIn
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
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.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -224,9 +226,12 @@ private fun RoomMemberActionsBottomSheet(
|
|||
onDismiss()
|
||||
}
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = user.getAvatarData(size = AvatarSize.RoomListManageUser),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.oidc.api)
|
||||
implementation(projects.libraries.oauth.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.testtags)
|
||||
api(libs.statemachine)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import io.element.android.libraries.architecture.createNode
|
|||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -123,12 +123,12 @@ class ResetIdentityFlowNode(
|
|||
null -> {
|
||||
Timber.d("No reset handle return, the reset is done.")
|
||||
}
|
||||
is IdentityOidcResetHandle -> {
|
||||
is IdentityOAuthResetHandle -> {
|
||||
Timber.d("Launching reset confirmation in MAS")
|
||||
activity.openUrlInChromeCustomTab(null, darkTheme, handle.url)
|
||||
Timber.d("Starting resetOidc")
|
||||
resetJob = launch { handle.resetOidc() }
|
||||
resetJob?.invokeOnCompletion { Timber.d("resetOidc ended") }
|
||||
Timber.d("Starting resetOAuth")
|
||||
resetJob = launch { handle.resetOAuth() }
|
||||
resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") }
|
||||
}
|
||||
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ private fun aSessionData(
|
|||
accessToken = "anAccessToken",
|
||||
refreshToken = "aRefreshToken",
|
||||
homeserverUrl = "aHomeserverUrl",
|
||||
oidcData = null,
|
||||
oAuthData = null,
|
||||
loginTimestamp = null,
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = LoginType.UNKNOWN,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ 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.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -52,11 +54,13 @@ fun JoinRoomByAddressView(
|
|||
onDismissRequest = {
|
||||
state.eventSink(JoinRoomByAddressEvent.Dismiss)
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp),
|
||||
.padding(all = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RoomAddressField(
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ telephoto = "0.19.0"
|
|||
haze = "1.7.2"
|
||||
|
||||
# Dependency analysis
|
||||
dependencyAnalysis = "3.7.0"
|
||||
dependencyAnalysis = "3.9.0"
|
||||
|
||||
# DI
|
||||
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
|
||||
# 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.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.21"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.27"
|
||||
|
||||
# Others
|
||||
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"
|
||||
showkase = { module = "com.airbnb.android:showkase", 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" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
|
|
@ -222,7 +222,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
|
|||
|
||||
# Analytics
|
||||
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
|
||||
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" }
|
||||
|
||||
# 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
|
||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ fun FlowStepPage(
|
|||
},
|
||||
header = {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
modifier = Modifier.padding(bottom = 16.dp, start = 8.dp, end = 8.dp),
|
||||
title = title,
|
||||
subTitle = subTitle,
|
||||
iconStyle = iconStyle,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ 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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -38,11 +40,13 @@ fun SimpleModalBottomSheet(
|
|||
onDismissRequest = onDismiss,
|
||||
modifier = modifier,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
scrollable = false,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -42,10 +45,15 @@ import io.element.android.libraries.designsystem.preview.sheetStateForPreview
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
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)
|
||||
@Composable
|
||||
fun ModalBottomSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
scrollable: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
|
|
@ -79,8 +87,17 @@ fun ModalBottomSheet(
|
|||
scrimColor = scrimColor,
|
||||
dragHandle = dragHandle,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
content = content,
|
||||
)
|
||||
) {
|
||||
if (scrollable) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
// This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380
|
||||
@Preview(group = PreviewGroup.BottomSheets)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetDarkPreview() =
|
||||
|
|
@ -112,6 +127,7 @@ private fun ContentToPreview() {
|
|||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {},
|
||||
scrollable = false,
|
||||
) {
|
||||
Text(
|
||||
text = "Sheet Content",
|
||||
|
|
|
|||
|
|
@ -77,26 +77,28 @@ class DefaultPinnedMessagesBannerFormatter(
|
|||
messageType.toPlainText(permalinkParser)
|
||||
}
|
||||
is VideoMessageType -> {
|
||||
messageType.bestDescription.prefixWith(CommonStrings.common_video)
|
||||
messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_video)
|
||||
}
|
||||
is ImageMessageType -> {
|
||||
messageType.bestDescription.prefixWith(CommonStrings.common_image)
|
||||
messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_image)
|
||||
}
|
||||
is StickerMessageType -> {
|
||||
messageType.bestDescription.prefixWith(CommonStrings.common_sticker)
|
||||
messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_sticker)
|
||||
}
|
||||
is LocationMessageType -> {
|
||||
messageType.body.prefixWith(CommonStrings.common_shared_location)
|
||||
}
|
||||
is FileMessageType -> {
|
||||
messageType.bestDescription.prefixWith(CommonStrings.common_file)
|
||||
messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_file)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
messageType.bestDescription.prefixWith(CommonStrings.common_audio)
|
||||
messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_audio)
|
||||
}
|
||||
is VoiceMessageType -> {
|
||||
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available.
|
||||
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||
messageType
|
||||
.toPlainText(permalinkParser, "")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||
?: sp.getString(CommonStrings.common_voice_message)
|
||||
}
|
||||
is OtherMessageType -> {
|
||||
|
|
|
|||
|
|
@ -139,26 +139,28 @@ class DefaultRoomLatestEventFormatter(
|
|||
messageType.toPlainText(permalinkParser)
|
||||
}
|
||||
is VideoMessageType -> {
|
||||
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_video))
|
||||
messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_video))
|
||||
}
|
||||
is ImageMessageType -> {
|
||||
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_image))
|
||||
messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_image))
|
||||
}
|
||||
is StickerMessageType -> {
|
||||
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker))
|
||||
messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_sticker))
|
||||
}
|
||||
is LocationMessageType -> {
|
||||
sp.getString(CommonStrings.common_shared_location)
|
||||
}
|
||||
is FileMessageType -> {
|
||||
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_file))
|
||||
messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_file))
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_audio))
|
||||
messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_audio))
|
||||
}
|
||||
is VoiceMessageType -> {
|
||||
// In this case, do not use bestDescription, because the filename is useless, only use the caption if available.
|
||||
messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||
messageType
|
||||
.toPlainText(permalinkParser, "")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.prefixWith(sp.getString(CommonStrings.common_voice_message))
|
||||
?: sp.getString(CommonStrings.common_voice_message)
|
||||
}
|
||||
is OtherMessageType -> {
|
||||
|
|
|
|||
|
|
@ -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.notification.NotificationService
|
||||
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.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ sealed class AuthenticationException(message: String?) : Exception(message) {
|
|||
class InvalidServerName(message: String?) : AuthenticationException(message)
|
||||
class SlidingSyncVersion(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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,21 +37,21 @@ interface MatrixAuthenticationService {
|
|||
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(
|
||||
prompt: OidcPrompt,
|
||||
suspend fun getOAuthUrl(
|
||||
prompt: OAuthPrompt,
|
||||
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.
|
||||
|
|
@ -68,9 +68,9 @@ interface MatrixAuthenticationService {
|
|||
): 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.api.auth
|
|||
data class MatrixHomeServerDetails(
|
||||
val url: String,
|
||||
val supportsPasswordLogin: Boolean,
|
||||
val supportsOidcLogin: Boolean,
|
||||
val supportsOAuthLogin: Boolean,
|
||||
) {
|
||||
val isSupported = supportsPasswordLogin || supportsOidcLogin
|
||||
val isSupported = supportsPasswordLogin || supportsOAuthLogin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.api.auth
|
|||
|
||||
import io.element.android.libraries.matrix.api.BuildConfig
|
||||
|
||||
object OidcConfig {
|
||||
object OAuthConfig {
|
||||
const val CLIENT_URI = BuildConfig.CLIENT_URI
|
||||
|
||||
// Note: host must match with the host of CLIENT_URI
|
||||
|
|
@ -12,6 +12,6 @@ import android.os.Parcelable
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class OidcDetails(
|
||||
data class OAuthDetails(
|
||||
val url: String,
|
||||
) : Parcelable
|
||||
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
sealed interface OidcPrompt {
|
||||
sealed interface OAuthPrompt {
|
||||
/**
|
||||
* The Authorization Server should prompt the End-User for
|
||||
* reauthentication.
|
||||
*/
|
||||
data object Login : OidcPrompt
|
||||
data object Login : OAuthPrompt
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
data object Create : OidcPrompt
|
||||
data object Create : OAuthPrompt
|
||||
|
||||
/**
|
||||
* An unknown value.
|
||||
*/
|
||||
data class Unknown(val value: String) : OidcPrompt
|
||||
data class Unknown(val value: String) : OAuthPrompt
|
||||
}
|
||||
|
|
@ -8,6 +8,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
interface OidcRedirectUrlProvider {
|
||||
interface OAuthRedirectUrlProvider {
|
||||
fun provide(): String
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ sealed class QrLoginException : Exception() {
|
|||
data object Expired : QrLoginException()
|
||||
data object NotFound : QrLoginException()
|
||||
data object LinkingNotSupported : QrLoginException()
|
||||
data object OidcMetadataInvalid : QrLoginException()
|
||||
data object OAuthMetadataInvalid : QrLoginException()
|
||||
data object SlidingSyncNotAvailable : QrLoginException()
|
||||
data object OtherDeviceNotSignedIn : QrLoginException()
|
||||
data object CheckCodeAlreadySent : QrLoginException()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
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
|
||||
* called, or the identity is reset.
|
||||
*/
|
||||
suspend fun resetOidc(): Result<Unit>
|
||||
suspend fun resetOAuth(): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* 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
|
||||
|
||||
|
|
@ -196,9 +196,9 @@ interface JoinedRoom : BaseRoom {
|
|||
/**
|
||||
* Start sharing live location in this room.
|
||||
* @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.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class MatrixHomeServerDetailsTest {
|
|||
@Test
|
||||
fun `if homeserver supports oidc, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = true,
|
||||
supportsOAuthLogin = true,
|
||||
supportsPasswordLogin = false,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
|
|
@ -25,7 +25,7 @@ class MatrixHomeServerDetailsTest {
|
|||
@Test
|
||||
fun `if homeserver supports password, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = false,
|
||||
supportsOAuthLogin = false,
|
||||
supportsPasswordLogin = true,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
|
|
@ -34,7 +34,7 @@ class MatrixHomeServerDetailsTest {
|
|||
@Test
|
||||
fun `if homeserver supports both, then it is supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = true,
|
||||
supportsOAuthLogin = true,
|
||||
supportsPasswordLogin = true,
|
||||
)
|
||||
assertThat(sut.isSupported).isTrue()
|
||||
|
|
@ -43,7 +43,7 @@ class MatrixHomeServerDetailsTest {
|
|||
@Test
|
||||
fun `if homeserver supports none, then it is not supported`() {
|
||||
val sut = aMatrixHomeServerDetails(
|
||||
supportsOidcLogin = false,
|
||||
supportsOAuthLogin = false,
|
||||
supportsPasswordLogin = false,
|
||||
)
|
||||
assertThat(sut.isSupported).isFalse()
|
||||
|
|
|
|||
|
|
@ -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.LinkMobileHandler
|
||||
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.CurrentUserMembership
|
||||
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.notification.RustNotificationService
|
||||
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.room.GetRoomResult
|
||||
import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom
|
||||
|
|
|
|||
|
|
@ -214,5 +214,5 @@ fun SessionData.toSession() = Session(
|
|||
deviceId = deviceId,
|
||||
homeserverUrl = homeserverUrl,
|
||||
slidingSyncVersion = SlidingSyncVersion.NATIVE,
|
||||
oidcData = oidcData,
|
||||
oauthData = oAuthData,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.auth
|
|||
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import org.matrix.rustcomponents.sdk.ClientBuildException
|
||||
import org.matrix.rustcomponents.sdk.OidcException
|
||||
import org.matrix.rustcomponents.sdk.OAuthException
|
||||
|
||||
fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
return when (this) {
|
||||
|
|
@ -29,12 +29,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
|||
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
|
||||
}
|
||||
is OidcException -> when (this) {
|
||||
is OidcException.Generic -> AuthenticationException.Oidc(message)
|
||||
is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message)
|
||||
is OidcException.Cancelled -> AuthenticationException.Oidc(message)
|
||||
is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message)
|
||||
is OidcException.NotSupported -> AuthenticationException.Oidc(message)
|
||||
is OAuthException -> when (this) {
|
||||
is OAuthException.Generic -> AuthenticationException.OAuth(message)
|
||||
is OAuthException.CallbackUrlInvalid -> AuthenticationException.OAuth(message)
|
||||
is OAuthException.Cancelled -> AuthenticationException.OAuth(message)
|
||||
is OAuthException.MetadataInvalid -> AuthenticationException.OAuth(message)
|
||||
is OAuthException.NotSupported -> AuthenticationException.OAuth(message)
|
||||
}
|
||||
else -> AuthenticationException.Generic(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
|
|||
MatrixHomeServerDetails(
|
||||
url = url(),
|
||||
supportsPasswordLogin = supportsPasswordLogin(),
|
||||
supportsOidcLogin = supportsOidcLogin(),
|
||||
supportsOAuthLogin = supportsOauthLogin(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -8,13 +8,13 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcPrompt
|
||||
import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthPrompt
|
||||
import org.matrix.rustcomponents.sdk.OAuthPrompt as RustOAuthPrompt
|
||||
|
||||
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt {
|
||||
internal fun OAuthPrompt.toRustPrompt(): RustOAuthPrompt {
|
||||
return when (this) {
|
||||
OidcPrompt.Login -> RustOidcPrompt.Unknown("consent")
|
||||
OidcPrompt.Create -> RustOidcPrompt.Create
|
||||
is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value)
|
||||
OAuthPrompt.Login -> RustOAuthPrompt.Unknown("consent")
|
||||
OAuthPrompt.Create -> RustOAuthPrompt.Create
|
||||
is OAuthPrompt.Unknown -> RustOAuthPrompt.Unknown(value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class RustHomeServerLoginCompatibilityChecker(
|
|||
it.homeserverLoginDetails()
|
||||
}
|
||||
.use {
|
||||
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
|
||||
it.supportsOidcLogin() || it.supportsPasswordLogin()
|
||||
Timber.d("Homeserver $url | OAuth: ${it.supportsOauthLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
|
||||
it.supportsOauthLogin() || it.supportsPasswordLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.MatrixAuthenticationService
|
||||
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.OidcPrompt
|
||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||
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.external.ExternalSession
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
|
|
@ -65,7 +65,7 @@ class RustMatrixAuthenticationService(
|
|||
private val sessionStore: SessionStore,
|
||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||
private val passphraseGenerator: PassphraseGenerator,
|
||||
private val oidcConfigurationProvider: OidcConfigurationProvider,
|
||||
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
|
||||
) : MatrixAuthenticationService {
|
||||
// Any existing Element Classic session that we want to try to import secrets from during login.
|
||||
private var elementClassicSession: ElementClassicSession? = null
|
||||
|
|
@ -253,15 +253,15 @@ class RustMatrixAuthenticationService(
|
|||
|
||||
private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null
|
||||
|
||||
override suspend fun getOidcUrl(
|
||||
prompt: OidcPrompt,
|
||||
override suspend fun getOAuthUrl(
|
||||
prompt: OAuthPrompt,
|
||||
loginHint: String?,
|
||||
): Result<OidcDetails> {
|
||||
): Result<OAuthDetails> {
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
||||
val oAuthAuthorizationData = client.urlForOidc(
|
||||
oidcConfiguration = oidcConfigurationProvider.get(),
|
||||
val oAuthAuthorizationData = client.urlForOauth(
|
||||
oauthConfiguration = oAuthConfigurationProvider.get(),
|
||||
prompt = prompt.toRustPrompt(),
|
||||
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
|
||||
|
|
@ -270,23 +270,23 @@ class RustMatrixAuthenticationService(
|
|||
)
|
||||
val url = oAuthAuthorizationData.loginUrl()
|
||||
pendingOAuthAuthorizationData = oAuthAuthorizationData
|
||||
OidcDetails(url)
|
||||
OAuthDetails(url)
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to get OIDC URL")
|
||||
Timber.e(failure, "Failed to get OAuth URL")
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||
override suspend fun cancelOAuthLogin(): Result<Unit> {
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
pendingOAuthAuthorizationData?.use {
|
||||
currentClient?.abortOidcAuth(it)
|
||||
currentClient?.abortOauthAuth(it)
|
||||
}
|
||||
pendingOAuthAuthorizationData = null
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to cancel OIDC login")
|
||||
Timber.e(failure, "Failed to cancel OAuth login")
|
||||
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) {
|
||||
runCatchingExceptions {
|
||||
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
||||
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
|
||||
client.loginWithOidcCallback(
|
||||
client.loginWithOauthCallback(
|
||||
callbackUrl = callbackUrl,
|
||||
)
|
||||
// Free the pending data since we won't use it to abort the flow anymore
|
||||
|
|
@ -330,7 +330,7 @@ class RustMatrixAuthenticationService(
|
|||
|
||||
SessionId(sessionData.userId)
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to login with OIDC")
|
||||
Timber.e(failure, "Failed to login with OAuth")
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
|
|
@ -355,7 +355,7 @@ class RustMatrixAuthenticationService(
|
|||
withContext(coroutineDispatchers.io) {
|
||||
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
|
||||
val emptySessionPaths = rotateSessionPath()
|
||||
val oidcConfiguration = oidcConfigurationProvider.get()
|
||||
val oAuthConfiguration = oAuthConfigurationProvider.get()
|
||||
val progressListener = object : QrLoginProgressListener {
|
||||
override fun onUpdate(state: QrLoginProgress) {
|
||||
Timber.d("QR Code login progress: $state")
|
||||
|
|
@ -368,7 +368,7 @@ class RustMatrixAuthenticationService(
|
|||
qrCodeData = sdkQrCodeLoginData,
|
||||
)
|
||||
client.newLoginWithQrCodeHandler(
|
||||
oidcConfiguration = oidcConfiguration,
|
||||
oauthConfiguration = oAuthConfiguration,
|
||||
).use {
|
||||
it.scan(
|
||||
qrCodeData = qrCodeData.rustQrCodeData,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ object QrErrorMapper {
|
|||
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
|
||||
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
|
||||
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
|
||||
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid
|
||||
is RustHumanQrLoginException.OAuthMetadataInvalid -> QrLoginException.OAuthMetadataInvalid
|
||||
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
|
||||
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
|
||||
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent
|
||||
|
|
|
|||
|
|
@ -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.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.IdentityResetHandle
|
||||
import org.matrix.rustcomponents.sdk.AuthData
|
||||
|
|
@ -25,7 +25,7 @@ object RustIdentityResetHandleFactory {
|
|||
return runCatchingExceptions {
|
||||
identityResetHandle?.let {
|
||||
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)
|
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||
}
|
||||
|
|
@ -47,11 +47,11 @@ class RustPasswordIdentityResetHandle(
|
|||
}
|
||||
}
|
||||
|
||||
class RustOidcIdentityResetHandle(
|
||||
class RustIdentityOAuthResetHandle(
|
||||
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
|
||||
override val url: String,
|
||||
) : IdentityOidcResetHandle {
|
||||
override suspend fun resetOidc(): Result<Unit> {
|
||||
) : IdentityOAuthResetHandle {
|
||||
override suspend fun resetOAuth(): Result<Unit> {
|
||||
return runCatchingExceptions { identityResetHandle.reset(null) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
Timber.tag(tag.value).w(e, "Invalid QR code scanned")
|
||||
_linkDesktopStep.emit(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
||||
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ internal fun Session.toSessionData(
|
|||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl ?: this.homeserverUrl,
|
||||
oidcData = oidcData,
|
||||
oAuthData = oauthData,
|
||||
loginTimestamp = Date(),
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
|
|
@ -52,7 +52,7 @@ internal fun ExternalSession.toSessionData(
|
|||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl,
|
||||
oidcData = null,
|
||||
oAuthData = null,
|
||||
loginTimestamp = Date(),
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
* 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
|
||||
|
||||
fun AccountManagementAction.toRustAction(): RustAccountManagementAction {
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue