Oidc: Fallback to external browser instead of using Webview (#4808)
* Oidc: Fallback to external browser instead of using Webview * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
525870f30d
commit
88124cd770
21 changed files with 14 additions and 709 deletions
|
|
@ -36,6 +36,7 @@ import io.element.android.features.login.impl.screens.createaccount.CreateAccoun
|
||||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
|
||||||
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
|
import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode
|
||||||
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
|
||||||
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -45,7 +46,6 @@ import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||||
import io.element.android.libraries.oidc.api.OidcEntryPoint
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -57,7 +57,6 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
private val oidcEntryPoint: OidcEntryPoint,
|
|
||||||
) : BaseFlowNode<LoginFlowNode.NavTarget>(
|
) : BaseFlowNode<LoginFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.OnBoarding,
|
initialElement = NavTarget.OnBoarding,
|
||||||
|
|
@ -74,15 +73,15 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var darkTheme: Boolean = false
|
private var darkTheme: Boolean = false
|
||||||
|
|
||||||
private var customChromeTabStarted = false
|
private var externalAppStarted = false
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
defaultLoginUserStory.setLoginFlowIsDone(false)
|
defaultLoginUserStory.setLoginFlowIsDone(false)
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
if (customChromeTabStarted) {
|
if (externalAppStarted) {
|
||||||
customChromeTabStarted = false
|
externalAppStarted = false
|
||||||
// Workaround to detect that the Custom Chrome Tab has been closed
|
// Workaround to detect that the Custom Chrome Tab has been closed
|
||||||
// If there is no coming OidcAction (that would end this Node),
|
// If there is no coming OidcAction (that would end this Node),
|
||||||
// consider that the user has cancelled the login
|
// consider that the user has cancelled the login
|
||||||
|
|
@ -122,9 +121,6 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CreateAccount(val url: String) : NavTarget
|
data class CreateAccount(val url: String) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class OidcView(val oidcDetails: OidcDetails) : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -249,9 +245,6 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
NavTarget.LoginPassword -> {
|
NavTarget.LoginPassword -> {
|
||||||
createNode<LoginPasswordNode>(buildContext)
|
createNode<LoginPasswordNode>(buildContext)
|
||||||
}
|
}
|
||||||
is NavTarget.OidcView -> {
|
|
||||||
oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
|
|
||||||
}
|
|
||||||
is NavTarget.CreateAccount -> {
|
is NavTarget.CreateAccount -> {
|
||||||
val inputs = CreateAccountNode.Inputs(
|
val inputs = CreateAccountNode.Inputs(
|
||||||
url = navTarget.url,
|
url = navTarget.url,
|
||||||
|
|
@ -262,15 +255,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMas(oidcDetails: OidcDetails) {
|
private fun navigateToMas(oidcDetails: OidcDetails) {
|
||||||
if (oidcEntryPoint.canUseCustomTab()) {
|
activity?.let {
|
||||||
// In this case open a Chrome Custom tab
|
externalAppStarted = true
|
||||||
activity?.let {
|
it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url)
|
||||||
customChromeTabStarted = true
|
|
||||||
oidcEntryPoint.openUrlInCustomTab(it, darkTheme, oidcDetails.url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to WebView mode
|
|
||||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
|
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
|
||||||
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
|
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
|
|
@ -37,7 +38,6 @@ import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
|
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
|
||||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
import io.element.android.libraries.oidc.api.OidcEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
@ -51,7 +51,6 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val resetIdentityFlowManager: ResetIdentityFlowManager,
|
private val resetIdentityFlowManager: ResetIdentityFlowManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val oidcEntryPoint: OidcEntryPoint,
|
|
||||||
) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>(
|
) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>(
|
||||||
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
|
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -67,12 +66,10 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ResetPassword : NavTarget
|
data object ResetPassword : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ResetOidc(val url: String) : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var activity: Activity
|
private lateinit var activity: Activity
|
||||||
|
private var darkTheme: Boolean = false
|
||||||
private var resetJob: Job? = null
|
private var resetJob: Job? = null
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -80,7 +77,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
// If the custom tab was opened, we need to cancel the reset job
|
// If the custom tab / Web browser was opened, we need to cancel the reset job
|
||||||
// when we come back to the node if the reset wasn't successful
|
// when we come back to the node if the reset wasn't successful
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
cancelResetJob()
|
cancelResetJob()
|
||||||
|
|
@ -115,9 +112,6 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
listOf(ResetIdentityPasswordNode.Inputs(handle))
|
listOf(ResetIdentityPasswordNode.Inputs(handle))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NavTarget.ResetOidc -> {
|
|
||||||
oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,11 +129,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
Timber.d("No reset handle return, the reset is done.")
|
Timber.d("No reset handle return, the reset is done.")
|
||||||
}
|
}
|
||||||
is IdentityOidcResetHandle -> {
|
is IdentityOidcResetHandle -> {
|
||||||
if (oidcEntryPoint.canUseCustomTab()) {
|
activity.openUrlInChromeCustomTab(null, darkTheme, handle.url)
|
||||||
activity.openUrlInChromeCustomTab(null, false, handle.url)
|
|
||||||
} else {
|
|
||||||
backstack.push(NavTarget.ResetOidc(handle.url))
|
|
||||||
}
|
|
||||||
resetJob = launch { handle.resetOidc() }
|
resetJob = launch { handle.resetOidc() }
|
||||||
}
|
}
|
||||||
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
|
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
|
||||||
|
|
@ -162,7 +152,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
if (!this::activity.isInitialized) {
|
if (!this::activity.isInitialized) {
|
||||||
activity = requireNotNull(LocalActivity.current)
|
activity = requireNotNull(LocalActivity.current)
|
||||||
}
|
}
|
||||||
|
darkTheme = !ElementTheme.isLightTheme
|
||||||
val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()
|
val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()
|
||||||
if (startResetState.isLoading()) {
|
if (startResetState.isLoading()) {
|
||||||
ProgressDialog(
|
ProgressDialog(
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 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.oidc.api
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
|
|
||||||
interface OidcEntryPoint {
|
|
||||||
fun canUseCustomTab(): Boolean
|
|
||||||
fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String)
|
|
||||||
fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.browser.customtabs.CustomTabsClient
|
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class CustomTabAvailabilityChecker @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.oidc.impl.customtab
|
package io.element.android.libraries.oidc.impl
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 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.oidc.impl
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
|
||||||
import io.element.android.libraries.architecture.createNode
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|
||||||
import io.element.android.libraries.oidc.api.OidcEntryPoint
|
|
||||||
import io.element.android.libraries.oidc.impl.webview.OidcNode
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultOidcEntryPoint @Inject constructor(
|
|
||||||
private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
|
|
||||||
) : OidcEntryPoint {
|
|
||||||
override fun canUseCustomTab(): Boolean {
|
|
||||||
return customTabAvailabilityChecker.supportCustomTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) {
|
|
||||||
assert(canUseCustomTab()) { "Custom tab is not supported in this device." }
|
|
||||||
activity.openUrlInChromeCustomTab(null, darkTheme, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node {
|
|
||||||
assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." }
|
|
||||||
val inputs = OidcNode.Inputs(OidcDetails(url))
|
|
||||||
return parentNode.createNode<OidcNode>(buildContext, listOf(inputs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.customtab
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.browser.customtabs.CustomTabsClient
|
|
||||||
import androidx.browser.customtabs.CustomTabsServiceConnection
|
|
||||||
import androidx.browser.customtabs.CustomTabsSession
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import io.element.android.libraries.androidutils.browser.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
|
|
||||||
|
|
||||||
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.apply { 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(url.toUri(), null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disposeCustomTab() {
|
|
||||||
customTabsServiceConnection?.let { context.unbindService(it) }
|
|
||||||
customTabsServiceConnection = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun open(activity: Activity, darkTheme: Boolean, url: String) {
|
|
||||||
activity.openUrlInChromeCustomTab(customTabsSession, darkTheme, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
|
||||||
|
|
||||||
sealed interface OidcEvents {
|
|
||||||
data object Cancel : OidcEvents
|
|
||||||
data class OidcActionEvent(val oidcAction: OidcAction) : OidcEvents
|
|
||||||
data object ClearError : OidcEvents
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
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 dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|
||||||
import io.element.android.libraries.oidc.impl.OidcUrlParser
|
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
|
||||||
class OidcNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
presenterFactory: OidcPresenter.Factory,
|
|
||||||
private val oidcUrlParser: OidcUrlParser,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
data class Inputs(
|
|
||||||
val oidcDetails: OidcDetails,
|
|
||||||
) : NodeInputs
|
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
|
||||||
private val presenter = presenterFactory.create(inputs.oidcDetails)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
OidcView(
|
|
||||||
state = state,
|
|
||||||
oidcUrlParser = oidcUrlParser,
|
|
||||||
modifier = modifier,
|
|
||||||
onNavigateBack = ::navigateUp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class OidcPresenter @AssistedInject constructor(
|
|
||||||
@Assisted private val oidcDetails: OidcDetails,
|
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
|
||||||
) : Presenter<OidcState> {
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
fun create(oidcDetails: OidcDetails): OidcPresenter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun present(): OidcState {
|
|
||||||
var requestState: AsyncAction<Unit> by remember {
|
|
||||||
mutableStateOf(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
fun handleCancel() {
|
|
||||||
requestState = AsyncAction.Loading
|
|
||||||
localCoroutineScope.launch {
|
|
||||||
authenticationService.cancelOidcLogin()
|
|
||||||
.fold(
|
|
||||||
onSuccess = {
|
|
||||||
// Then go back
|
|
||||||
requestState = AsyncAction.Success(Unit)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
requestState = AsyncAction.Failure(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleSuccess(url: String) {
|
|
||||||
requestState = AsyncAction.Loading
|
|
||||||
localCoroutineScope.launch {
|
|
||||||
authenticationService.loginWithOidc(url)
|
|
||||||
.onFailure {
|
|
||||||
requestState = AsyncAction.Failure(it)
|
|
||||||
}
|
|
||||||
// On success, the node tree will be updated, there is nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleAction(action: OidcAction) {
|
|
||||||
when (action) {
|
|
||||||
OidcAction.GoBack -> handleCancel()
|
|
||||||
is OidcAction.Success -> handleSuccess(action.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: OidcEvents) {
|
|
||||||
when (event) {
|
|
||||||
OidcEvents.Cancel -> handleCancel()
|
|
||||||
is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction)
|
|
||||||
OidcEvents.ClearError -> requestState = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return OidcState(
|
|
||||||
oidcDetails = oidcDetails,
|
|
||||||
requestState = requestState,
|
|
||||||
eventSink = ::handleEvents
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|
||||||
|
|
||||||
data class OidcState(
|
|
||||||
val oidcDetails: OidcDetails,
|
|
||||||
val requestState: AsyncAction<Unit>,
|
|
||||||
val eventSink: (OidcEvents) -> Unit
|
|
||||||
)
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|
||||||
|
|
||||||
open class OidcStateProvider : PreviewParameterProvider<OidcState> {
|
|
||||||
override val values: Sequence<OidcState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aOidcState(),
|
|
||||||
aOidcState().copy(requestState = AsyncAction.Loading),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aOidcState() = OidcState(
|
|
||||||
oidcDetails = aOidcDetails(),
|
|
||||||
requestState = AsyncAction.Uninitialized,
|
|
||||||
eventSink = {}
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aOidcDetails() = OidcDetails(
|
|
||||||
url = "aUrl",
|
|
||||||
)
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
|
||||||
import io.element.android.libraries.oidc.impl.OidcUrlParser
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun OidcView(
|
|
||||||
state: OidcState,
|
|
||||||
oidcUrlParser: OidcUrlParser,
|
|
||||||
onNavigateBack: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val isPreview = LocalInspectionMode.current
|
|
||||||
var webView by remember { mutableStateOf<WebView?>(null) }
|
|
||||||
fun shouldOverrideUrl(url: String): Boolean {
|
|
||||||
val action = oidcUrlParser.parse(url)
|
|
||||||
if (action != null) {
|
|
||||||
state.eventSink.invoke(OidcEvents.OidcActionEvent(action))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val oidcWebViewClient = remember {
|
|
||||||
OidcWebViewClient(::shouldOverrideUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBack() {
|
|
||||||
if (webView?.canGoBack().orFalse()) {
|
|
||||||
webView?.goBack()
|
|
||||||
} else {
|
|
||||||
// To properly cancel Oidc login
|
|
||||||
state.eventSink.invoke(OidcEvents.Cancel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler { onBack() }
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = ::onBack)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { contentPadding ->
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier.padding(contentPadding),
|
|
||||||
factory = { context ->
|
|
||||||
WebView(context).apply {
|
|
||||||
if (!isPreview) {
|
|
||||||
webViewClient = oidcWebViewClient
|
|
||||||
settings.apply {
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
javaScriptEnabled = true
|
|
||||||
allowContentAccess = true
|
|
||||||
allowFileAccess = true
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
databaseEnabled = true
|
|
||||||
domStorageEnabled = true
|
|
||||||
}
|
|
||||||
loadUrl(state.oidcDetails.url)
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
webView = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
AsyncActionView(
|
|
||||||
async = state.requestState,
|
|
||||||
onSuccess = { onNavigateBack() },
|
|
||||||
onErrorDismiss = { state.eventSink(OidcEvents.ClearError) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview {
|
|
||||||
OidcView(
|
|
||||||
state = state,
|
|
||||||
oidcUrlParser = { null },
|
|
||||||
onNavigateBack = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
|
|
||||||
class OidcWebViewClient(
|
|
||||||
private val eventListener: WebViewEventListener,
|
|
||||||
) : WebViewClient() {
|
|
||||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
|
||||||
return shouldOverrideUrl(request.url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
|
||||||
return shouldOverrideUrl(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldOverrideUrl(url: String): Boolean {
|
|
||||||
return eventListener.shouldOverrideUrlLoading(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.oidc.impl.webview
|
|
||||||
|
|
||||||
fun interface WebViewEventListener {
|
|
||||||
/**
|
|
||||||
* Triggered when a Webview loads an url.
|
|
||||||
*
|
|
||||||
* @param url The url about to be rendered.
|
|
||||||
* @return true if the method needs to manage some custom handling
|
|
||||||
*/
|
|
||||||
fun shouldOverrideUrlLoading(url: String): Boolean
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.oidc.impl.customtab
|
package io.element.android.libraries.oidc.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
import io.element.android.libraries.oidc.api.OidcAction
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.oidc.impl.webview
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|
||||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
|
||||||
import io.element.android.libraries.oidc.api.OidcAction
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class OidcPresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
FakeMatrixAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA)
|
|
||||||
assertThat(initialState.requestState).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - go back`() = runTest {
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
FakeMatrixAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(OidcEvents.Cancel)
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - go back with failure`() = runTest {
|
|
||||||
val authenticationService = FakeMatrixAuthenticationService()
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
authenticationService,
|
|
||||||
)
|
|
||||||
authenticationService.givenOidcCancelError(AN_EXCEPTION)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(OidcEvents.Cancel)
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
|
||||||
// Note: in real life I do not think this can happen, and the app should not block the user.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - user cancels from webview`() = runTest {
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
FakeMatrixAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack))
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - login success`() = runTest {
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
FakeMatrixAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
|
||||||
// In this case, no success, the session is created and the node get destroyed.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - login error`() = runTest {
|
|
||||||
val authenticationService = FakeMatrixAuthenticationService()
|
|
||||||
val presenter = OidcPresenter(
|
|
||||||
A_OIDC_DATA,
|
|
||||||
authenticationService,
|
|
||||||
)
|
|
||||||
authenticationService.givenLoginError(AN_EXCEPTION)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
|
||||||
val errorState = awaitItem()
|
|
||||||
assertThat(errorState.requestState).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
|
||||||
errorState.eventSink.invoke(OidcEvents.ClearError)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246
|
|
||||||
size 5874
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9ff42261f4d991650578ceadaa13be0b1924c1af8f71cf10832f53c7d958827d
|
|
||||||
size 9317
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a
|
|
||||||
size 5807
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e6158b2d0447199de84156fbfdc7bd9aa1af7ff2092b498db254ded465605b76
|
|
||||||
size 8208
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue