Skip to content

3DS Issue – WebView “A payment process is still active” persists and triggers immediate failure after cancel #2362

@AX-NICOLAS

Description

@AX-NICOLAS

Description

We are experiencing an issue with the Adyen 3DS flow using the Adyen Components SDK.
When a 3DS challenge is triggered, a WebView appears with the title “A payment process is still active”, description “Awaiting completion”, and a Cancel button.
After this screen, the user is redirected to their bank’s 3DS authentication page.

The issue happens when the user taps Cancel on this WebView:

All subsequent attempts with the same card fail.

When retrying a 3DS challenge, the callback onAdditionalDetails is immediately triggered as soon as the WebView shows, without completing the challenge.

This causes a direct backend call that returns a “3DS not authenticated” error.

This leads to a poor user experience as users retry multiple times without understanding why it always fails.

Steps to Reproduce

The issue is consistently reproducible.

Steps to reproduce:

Start a payment requiring 3DS authentication.

Observe the WebView showing “A payment process is still active / Awaiting completion” with a Cancel button.

Tap Cancel.

Retry payment with the same card.

Notice that onAdditionalDetails is immediately triggered and the payment fails with 3DS not authenticated.

Screenshots or screen recordings can be provided upon request.

Logs and Crash Reports

No crash occurs.
The issue manifests as the onAdditionalDetails callback being called immediately upon WebView launch, before the 3DS challenge is completed.

Integration Information

Server-side integration: Sessions

Client-side integration: Components

SDK versions:

adyen3ds = 2.2.24

adyenCheckout = 5.13.1

Android versions affected: multiple (e.g., Android 12, 13)

Device models affected: multiple (Samsung, Pixel, etc.)

Additional Context

We have noticed the issue seems related to the GenericActionComponent instance being retained between 3DS attempts.
Even after leaving the 3DS screen, the same component instance appears to be reused.
This is likely because in our Compose implementation, the use of remember(action) does not recreate the component if the same action object reference is passed.

This persistent instance could cause stale 3DS state to be kept in memory by the SDK, resulting in the immediate triggering of onAdditionalDetails when retrying, as if the challenge was already completed.

We have tried adding cleanup by calling actionComponent.detach() inside the onDispose block, but the issue remains.

We suspect the SDK internally caches or reuses 3DS sessions after cancellation.
We would like to know:

Does the SDK cache or reuse a 3DS session after a cancel?

Is there a recommended way to fully reset or recreate the GenericActionComponent after a cancellation?

Currently, this bug causes all retries to fail immediately after a single cancel, blocking users from completing payments.

Code Snippet (plain text)

package com.electra.adyen

import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.adyen.checkout.action.core.GenericActionComponent
import com.adyen.checkout.adyen3ds2.adyen3DS2
import com.adyen.checkout.components.core.ActionComponentCallback
import com.adyen.checkout.components.core.CheckoutConfiguration
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.ui.core.AdyenComponentView
import com.electra.metrics.ColorsV3
import com.electra.shared.adyen.ActionComponentData
import com.electra.shared.adyen.AdyenComponentError
import com.electra.shared.adyen.AdyenEnvironment
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

private const val TYPE_THREE_DS_2 = "threeDS2"

@composable
fun AdyenComponentScreen(
action: Action,
appLinkUrl: String,
clientKey: String,
environment: AdyenEnvironment,
appIntents: Flow,
onSuccess: (ActionComponentData) -> Unit,
onError: (AdyenComponentError) -> Unit,
onLog: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val activity = context as ComponentActivity
val lifecycleOwner = LocalLifecycleOwner.current
val currentOnSuccess by rememberUpdatedState(onSuccess)
val currentOnError by rememberUpdatedState(onError)
val currentOnLog by rememberUpdatedState(onLog)

val paymentDataRemember = remember { action.paymentData }

val checkoutConfiguration = remember(action) {
CheckoutConfiguration(
environment = when (environment) {
AdyenEnvironment.TEST -> com.adyen.checkout.core.Environment.TEST
AdyenEnvironment.EUROPE -> com.adyen.checkout.core.Environment.EUROPE
},
clientKey = clientKey
) {
adyen3DS2 {
setThreeDSRequestorAppURL(appLinkUrl)
}
}
}

// We suspect this instance is retained between screens causing stale state.
val actionComponent = remember(action) {
currentOnLog("google_pay actionComponent created")
GenericActionComponent.PROVIDER.get(
activity = activity,
checkoutConfiguration = checkoutConfiguration,
object : ActionComponentCallback {
override fun onAdditionalDetails(
actionComponentData: com.adyen.checkout.components.core.ActionComponentData
) {
val paymentData = actionComponentData.paymentData ?: paymentDataRemember

            val actionComponentSerializer = try {  
                com.adyen.checkout.components.core.ActionComponentData.SERIALIZER.serialize(  
                    actionComponentData  
                ).get("details")  
            } catch (e: Exception) {  
                currentOnLog(  
                    "google_pay AdyenComponentScreen onAdditionalDetails problem with ActionComponentData.SERIALIZER $e"  
                )  
                actionComponentData.details  
            }  

            currentOnLog(  
                "google_pay AdyenComponentScreen onAdditionalDetails $actionComponentData $paymentDataRemember " +  
                    "$actionComponentSerializer"  
            )  

            currentOnSuccess(  
                ActionComponentData(  
                    paymentData = paymentData,  
                    details = actionComponentSerializer.toString()  
                )  
            )  
        }  

        override fun onError(componentError: com.adyen.checkout.components.core.ComponentError) {  
            currentOnError(AdyenComponentError(componentError.exception))  
        }  
    }  
)  

}

onLog("google_pay instance actionComponent -> $actionComponent")
onLog("google_pay instance actionComponent -> $appIntents")
onLog("google_pay instance actionComponent -> $action")

DisposableEffect(Unit) {
val job = appIntents
.filter {
val uri = it.data

        if (uri == null) {  
            currentOnLog("Adyen → intent.data is null")  
            return@filter false  
        }  

        val hasParams = uri.queryParameterNames.isNotEmpty()  
        val isMatch = uri.toString().startsWith(appLinkUrl)  
        val isThreeDs2 = action.type?.equals(TYPE_THREE_DS_2) ?: false  

        if (!hasParams) {  
            currentOnLog("Adyen → URI has no query parameters: $uri")  
        }  

        currentOnLog("Adyen → Deep link match=$isMatch, hasParams=$hasParams, uri=$uri")  

        isMatch && !isThreeDs2  
    }  
    .onEach { intent ->  
        actionComponent.handleIntent(intent)  
    }  
    .launchIn(scope)  

onDispose {  
    job.cancel()  
    actionComponent.detach()  
    currentOnLog("Adyen → actionComponent detached and disposed")  
}  

}

AndroidView(
modifier = Modifier
.fillMaxSize()
.background(ColorsV3.BackgroundLightDefault),
factory = {
AdyenComponentView(context = activity).apply {
attach(actionComponent, lifecycleOwner)
actionComponent.handleAction(action, activity)
}
}
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionIndicates issue only requires an answer to a question

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions