diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 22a63b60c9..875f9f0435 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -49,6 +49,14 @@
android:host="open"
android:scheme="elementx" />
+
+
+
+
+
+
+
+
(
backstack = BackStack(
@@ -204,8 +208,11 @@ class RootFlowNode @AssistedInject constructor(
}
suspend fun handleIntent(intent: Intent) {
- deeplinkParser.getFromIntent(intent)
- ?.let { navigateTo(it) }
+ val resolvedIntent = intentResolver.resolve(intent) ?: return
+ when (resolvedIntent) {
+ is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
+ is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
+ }
}
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
@@ -223,6 +230,10 @@ class RootFlowNode @AssistedInject constructor(
}
}
+ private fun onOidcAction(oidcAction: OidcAction) {
+ oidcActionFlow.post(oidcAction)
+ }
+
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
return attachChild {
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
new file mode 100644
index 0000000000..b567395c1e
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.appnav.intent
+
+import android.content.Intent
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcIntentResolver
+import io.element.android.libraries.deeplink.DeeplinkData
+import io.element.android.libraries.deeplink.DeeplinkParser
+import timber.log.Timber
+import javax.inject.Inject
+
+sealed interface ResolvedIntent {
+ data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
+ data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
+}
+
+class IntentResolver @Inject constructor(
+ private val deeplinkParser: DeeplinkParser,
+ private val oidcIntentResolver: OidcIntentResolver
+) {
+ fun resolve(intent: Intent): ResolvedIntent? {
+ val deepLinkData = deeplinkParser.getFromIntent(intent)
+ if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
+
+ val oidcAction = oidcIntentResolver.resolve(intent)
+ if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
+
+ // Unknown intent
+ Timber.w("Unknown intent")
+ return null
+ }
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
new file mode 100644
index 0000000000..6e90a390c4
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+sealed interface OidcAction {
+ object GoBack : OidcAction
+ data class Success(val url: String) : OidcAction
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
new file mode 100644
index 0000000000..004e7c8a51
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+interface OidcActionFlow {
+ fun post(oidcAction: OidcAction)
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
new file mode 100644
index 0000000000..a6ecf26fca
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+import android.content.Intent
+
+interface OidcIntentResolver {
+ fun resolve(intent: Intent): OidcAction?
+}
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 5666fc1179..c43a5c2cd3 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
+ implementation(libs.androidx.browser)
api(projects.features.login.api)
ksp(libs.showkase.processor)
diff --git a/features/login/impl/src/main/AndroidManifest.xml b/features/login/impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..172e8645c1
--- /dev/null
+++ b/features/login/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index 37cfec229b..f1f56888bb 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -29,6 +29,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.changeserver.ChangeServerNode
+import io.element.android.features.login.impl.oidc.CustomTabHandler
import io.element.android.features.login.impl.oidc.OidcNode
import io.element.android.features.login.impl.root.LoginRootNode
import io.element.android.libraries.architecture.BackstackNode
@@ -42,6 +43,7 @@ import kotlinx.parcelize.Parcelize
class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
+ private val customTabHandler: CustomTabHandler,
) : BackstackNode(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -50,7 +52,6 @@ class LoginFlowNode @AssistedInject constructor(
buildContext = buildContext,
plugins = plugins,
) {
-
sealed interface NavTarget : Parcelable {
@Parcelize
object Root : NavTarget
@@ -71,7 +72,12 @@ class LoginFlowNode @AssistedInject constructor(
}
override fun onOidcDetails(oidcDetails: OidcDetails) {
- backstack.push(NavTarget.OidcView(oidcDetails))
+ if (customTabHandler.supportCustomTab()) {
+ customTabHandler.open(oidcDetails.url)
+ } else {
+ // Fallback to WebView mode
+ backstack.push(NavTarget.OidcView(oidcDetails))
+ }
}
}
createNode(buildContext, plugins = listOf(callback))
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt
new file mode 100644
index 0000000000..593469a12d
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import android.content.ComponentName
+import android.content.Context
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsServiceConnection
+import androidx.browser.customtabs.CustomTabsSession
+import io.element.android.features.login.impl.oidc.web.openUrlInChromeCustomTab
+import io.element.android.libraries.di.ApplicationContext
+import javax.inject.Inject
+
+class CustomTabHandler @Inject constructor(
+ @ApplicationContext private val context: Context,
+) {
+ private var customTabsSession: CustomTabsSession? = null
+ private var customTabsClient: CustomTabsClient? = null
+ private var customTabsServiceConnection: CustomTabsServiceConnection? = null
+
+ /**
+ * Return true if the device supports Custom tab, i.e. there is an third party app with
+ * CustomTab support (ex: Chrome, Firefox, etc.).
+ */
+ fun supportCustomTab(): Boolean {
+ val packageName = CustomTabsClient.getPackageName(context, null)
+ return packageName != null
+ }
+
+ fun prepareCustomTab(url: String) {
+ val packageName = CustomTabsClient.getPackageName(context, null)
+
+ // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
+ if (packageName != null) {
+ customTabsServiceConnection = object : CustomTabsServiceConnection() {
+ override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
+ customTabsClient = client
+ .also { it.warmup(0L) }
+ prefetchUrl(url)
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ }
+ }
+ .also {
+ CustomTabsClient.bindCustomTabsService(
+ context,
+ // Despite the API, packageName cannot be null
+ packageName,
+ it
+ )
+ }
+ }
+ }
+
+ private fun prefetchUrl(url: String) {
+ if (customTabsSession == null) {
+ customTabsSession = customTabsClient?.newSession(null)
+ }
+
+ customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
+ }
+
+ fun disposeCustomTab() {
+ customTabsServiceConnection?.let { context.unbindService(it) }
+ customTabsServiceConnection = null
+ }
+
+ fun open(url: String) {
+ openUrlInChromeCustomTab(context, customTabsSession, false, url)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
new file mode 100644
index 0000000000..8b6844e0f3
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import android.content.Intent
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcIntentResolver
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultOidcIntentResolver @Inject constructor(
+ private val oidcUrlParser: OidcUrlParser,
+) : OidcIntentResolver {
+ override fun resolve(intent: Intent): OidcAction? {
+ return oidcUrlParser.parse(intent.dataString.orEmpty())
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt
index 0c40f457bf..4f62c6476d 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt
@@ -16,6 +16,8 @@
package io.element.android.features.login.impl.oidc
+import io.element.android.features.login.api.oidc.OidcAction
+
sealed interface OidcEvents {
object Cancel : OidcEvents
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt
index 1c7debfd6f..f66caef5b2 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
index 090fd62501..487df70253 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
@@ -16,13 +16,15 @@
package io.element.android.features.login.impl.oidc
+import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.matrix.api.auth.OidcConfig
+import javax.inject.Inject
/**
* Simple parser for oidc url interception.
* TODO Find documentation about the format.
*/
-class OidcUrlParser {
+class OidcUrlParser @Inject constructor() {
// When user press button "Cancel", we get the url:
// `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
@@ -40,8 +42,3 @@ class OidcUrlParser {
error("Not supported: $url")
}
}
-
-sealed interface OidcAction {
- object GoBack : OidcAction
- data class Success(val url: String) : OidcAction
-}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt
new file mode 100644
index 0000000000..d6c9de7e68
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.web
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsSession
+
+/**
+ * Open url in custom tab or, if not available, in the default browser.
+ * If several compatible browsers are installed, the user will be proposed to choose one.
+ * Ref: https://developer.chrome.com/multidevice/android/customtabs.
+ */
+fun openUrlInChromeCustomTab(
+ context: Context,
+ session: CustomTabsSession?,
+ darkTheme: Boolean,
+ url: String
+) {
+ try {
+ CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(
+ CustomTabColorSchemeParams.Builder()
+ // TODO .setToolbarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
+ // TODO .setNavigationBarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
+ .build()
+ )
+ .setColorScheme(
+ when (darkTheme) {
+ false -> CustomTabsIntent.COLOR_SCHEME_LIGHT
+ true -> CustomTabsIntent.COLOR_SCHEME_DARK
+ }
+ )
+ // Note: setting close button icon does not work
+ // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
+ // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
+ // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
+ .apply { session?.let { setSession(it) } }
+ .build()
+ .apply {
+ intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ .launchUrl(context, Uri.parse(url))
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ // TODO context.toast(R.string.error_no_external_application_found)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt
new file mode 100644
index 0000000000..be4ec88ca2
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.web
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcActionFlow
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SingleIn
+import kotlinx.coroutines.flow.MutableStateFlow
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+@SingleIn(AppScope::class)
+class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow {
+ private val mutableStateFlow = MutableStateFlow(null)
+
+ override fun post(oidcAction: OidcAction) {
+ mutableStateFlow.value = oidcAction
+ }
+
+ suspend fun collect(lambda: suspend (OidcAction?) -> Unit) {
+ mutableStateFlow.collect(lambda)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
index 5dec3b6537..5bf9f2ad05 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
@@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.impl.oidc.web.DefaultOidcActionFlow
import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
@@ -36,6 +38,7 @@ import javax.inject.Inject
class LoginRootPresenter @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
+ private val defaultOidcActionFlow: DefaultOidcActionFlow,
) : Presenter {
@Composable
@@ -64,6 +67,14 @@ class LoginRootPresenter @Inject constructor(
mutableStateOf(LoginFormState.Default)
}
+ LaunchedEffect(Unit) {
+ launch {
+ defaultOidcActionFlow.collect {
+ onOidcAction(it, loggedInState)
+ }
+ }
+ }
+
fun handleEvents(event: LoginRootEvents) {
when (event) {
LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction)
@@ -131,4 +142,29 @@ class LoginRootPresenter @Inject constructor(
private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) {
formState.value = updateLambda(formState.value)
}
+
+ private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) {
+ oidcAction ?: return
+ loggedInState.value = LoggedInState.LoggingIn
+ when (oidcAction) {
+ OidcAction.GoBack -> {
+ authenticationService.cancelOidcLogin()
+ .onSuccess {
+ loggedInState.value = LoggedInState.NotLoggedIn
+ }
+ .onFailure { failure ->
+ loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
+ }
+ }
+ is OidcAction.Success -> {
+ authenticationService.loginWithOidc(oidcAction.url)
+ .onSuccess { sessionId ->
+ loggedInState.value = LoggedInState.LoggedIn(sessionId)
+ }
+ .onFailure { failure ->
+ loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
+ }
+ }
+ }
+ }
}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt
index 0fba87f2e3..f69cecd0f2 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt
@@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
index d13f24c885..a0275f8f47 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
@@ -17,6 +17,7 @@
package io.element.android.features.login.impl.oidc
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.matrix.api.auth.OidcConfig
import org.junit.Assert
import org.junit.Test
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9783cb1cf7..8e3522055c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,6 +18,7 @@ lifecycle = "2.6.1"
activity = "1.7.2"
startup = "1.1.1"
media3 = "1.0.2"
+browser = "1.5.0"
# Compose
compose_bom = "2023.05.01"
@@ -70,6 +71,7 @@ androidx_datastore_datastore = { module = "androidx.datastore:datastore", versio
androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+androidx_browser = { module = "androidx.browser:browser", version.ref = "browser" }
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts
index d850074d66..08ec84c227 100644
--- a/libraries/deeplink/build.gradle.kts
+++ b/libraries/deeplink/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
implementation(libs.dagger)
implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.architecture)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
diff --git a/tools/adb/oidc.sh b/tools/adb/oidc.sh
new file mode 100755
index 0000000000..52be556cbb
--- /dev/null
+++ b/tools/adb/oidc.sh
@@ -0,0 +1,24 @@
+#! /bin/bash
+#
+# Copyright (c) 2023 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Format is:
+
+# Error
+# adb shell am start -a android.intent.action.VIEW -d io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO
+
+# Success
+adb shell am start -a android.intent.action.VIEW -d io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB