Browse Source

Oidc with CustomTab

feature/fga/small_timeline_improvements
Benoit Marty 1 year ago committed by Benoit Marty
parent
commit
d2f969252d
  1. 8
      app/src/main/AndroidManifest.xml
  2. 19
      appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
  3. 47
      appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
  4. 22
      features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
  5. 21
      features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
  6. 23
      features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
  7. 1
      features/login/impl/build.gradle.kts
  8. 26
      features/login/impl/src/main/AndroidManifest.xml
  9. 10
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
  10. 87
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt
  11. 33
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
  12. 2
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt
  13. 1
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt
  14. 9
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
  15. 65
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt
  16. 39
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt
  17. 36
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
  18. 1
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt
  19. 1
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
  20. 2
      gradle/libs.versions.toml
  21. 1
      libraries/deeplink/build.gradle.kts
  22. 24
      tools/adb/oidc.sh

8
app/src/main/AndroidManifest.xml

@ -49,6 +49,14 @@ @@ -49,6 +49,14 @@
android:host="open"
android:scheme="elementx" />
</intent-filter-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="io.element" />
</intent-filter>
</activity>
<provider

19
appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt

@ -38,14 +38,17 @@ import dagger.assisted.Assisted @@ -38,14 +38,17 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.MatrixClientsHolder
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcActionFlow
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -65,7 +68,8 @@ class RootFlowNode @AssistedInject constructor( @@ -65,7 +68,8 @@ class RootFlowNode @AssistedInject constructor(
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
private val deeplinkParser: DeeplinkParser,
private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow,
) :
BackstackNode<RootFlowNode.NavTarget>(
backstack = BackStack(
@ -204,8 +208,11 @@ class RootFlowNode @AssistedInject constructor( @@ -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( @@ -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))

47
appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt

@ -0,0 +1,47 @@ @@ -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
}
}

22
features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt

@ -0,0 +1,22 @@ @@ -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
}

21
features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt

@ -0,0 +1,21 @@ @@ -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)
}

23
features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt

@ -0,0 +1,23 @@ @@ -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?
}

1
features/login/impl/build.gradle.kts

@ -45,6 +45,7 @@ dependencies { @@ -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)

26
features/login/impl/src/main/AndroidManifest.xml

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<!-- To open URL in CustomTab (prefetch, etc.). It makes CustomTabsClient.getPackageName() work
see https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>

10
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt

@ -29,6 +29,7 @@ import dagger.assisted.Assisted @@ -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 @@ -42,6 +43,7 @@ import kotlinx.parcelize.Parcelize
class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val customTabHandler: CustomTabHandler,
) : BackstackNode<LoginFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
@ -50,7 +52,6 @@ class LoginFlowNode @AssistedInject constructor( @@ -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( @@ -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<LoginRootNode>(buildContext, plugins = listOf(callback))

87
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt

@ -0,0 +1,87 @@ @@ -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)
}
}

33
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt

@ -0,0 +1,33 @@ @@ -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())
}
}

2
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt

@ -16,6 +16,8 @@ @@ -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

1
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt

@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue @@ -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

9
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt

@ -16,13 +16,15 @@ @@ -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 { @@ -40,8 +42,3 @@ class OidcUrlParser {
error("Not supported: $url")
}
}
sealed interface OidcAction {
object GoBack : OidcAction
data class Success(val url: String) : OidcAction
}

65
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt

@ -0,0 +1,65 @@ @@ -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)
}
}

39
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt

@ -0,0 +1,39 @@ @@ -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<OidcAction?>(null)
override fun post(oidcAction: OidcAction) {
mutableStateFlow.value = oidcAction
}
suspend fun collect(lambda: suspend (OidcAction?) -> Unit) {
mutableStateFlow.collect(lambda)
}
}

36
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt

@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf @@ -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 @@ -36,6 +38,7 @@ import javax.inject.Inject
class LoginRootPresenter @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val defaultOidcActionFlow: DefaultOidcActionFlow,
) : Presenter<LoginRootState> {
@Composable
@ -64,6 +67,14 @@ class LoginRootPresenter @Inject constructor( @@ -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( @@ -131,4 +142,29 @@ class LoginRootPresenter @Inject constructor(
private fun updateFormState(formState: MutableState<LoginFormState>, updateLambda: LoginFormState.() -> LoginFormState) {
formState.value = updateLambda(formState.value)
}
private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState<LoggedInState>) {
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)
}
}
}
}
}

1
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt

@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock @@ -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

1
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt

@ -17,6 +17,7 @@ @@ -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

2
gradle/libs.versions.toml

@ -18,6 +18,7 @@ lifecycle = "2.6.1" @@ -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 @@ -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"

1
libraries/deeplink/build.gradle.kts

@ -31,6 +31,7 @@ dependencies { @@ -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)

24
tools/adb/oidc.sh

@ -0,0 +1,24 @@ @@ -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
Loading…
Cancel
Save