stripe / stripe-android

Stripe Android SDK
https://stripe.com/docs/mobile/android
MIT License
1.3k stars 649 forks source link

[BUG] 3ds window not opening on Android when using payment intent #9659

Open derynia opened 1 week ago

derynia commented 1 week ago

Summary

I have the following problem, processing payment with 3ds card. My app uses Jetpack compose I have a payment screen. So I create a payment intent with new or saved card. Then I check if I need 3ds and start 3ds processing the following way:

Code to reproduce

@Composable
fun ThreeDSScreen(
    publishableKey: String,
    secret: String,
    onSuccess: () -> Unit,
    onCancel: () -> Unit,
    onFail: () -> Unit
) {
    val paymentLauncher = rememberPaymentLauncher(
        publishableKey = publishableKey,
        stripeAccountId = null,
        callback = PaymentResultCallback(
            onSuccess = onSuccess,
            onCancel = onCancel,
            onFail = onFail
        )
    )
    val needLaunch = remember { mutableStateOf(true) }

    LaunchedEffect(secret) {
        if (needLaunch.value) {
            needLaunch.value = false
            delay(200)
            paymentLauncher.confirm(
                ConfirmPaymentIntentParams.create(
                    clientSecret = secret,
                    paymentMethodType = PaymentMethod.Type.CardPresent
                )
            )
        }
    }
}

class PaymentResultCallback(
    private val onSuccess: () -> Unit,
    private val onCancel: () -> Unit,
    private val onFail: () -> Unit
) : PaymentLauncher.PaymentResultCallback {
    override fun onPaymentResult(paymentResult: PaymentResult) {
        when (paymentResult) {
            is PaymentResult.Completed -> { onSuccess() }
            is PaymentResult.Canceled -> { onCancel() }
            is PaymentResult.Failed -> { onFail() }
        }
    }
}

So everything works if I'm doing it first time. I create a card or use saved, I create a payment intent and get it's secret and then I call this code above. And the screen works fine. But then I'm stopping app on 3ds confirmation screen. I rerun app and go to the Store menu in my app. When starting it checks payment intent and if it exists and requires confirmation I ask user whether he wants to retry. User press yes and stripe confirmation window starts opening and closes at once. I'm getting Fail from Stripe. So I'm trying to cancel intent. In my example cancelling didn't succeeded. So I entered my store once again. Again it checks for pending payment intents. It asked again if I want to retry and from the second attempt Stripe confirmation window opened successfully.

Android version

All versions

Impacted devices

All deviices

Installation method

Build from Android studio

Dependency Versions

21.0.1

SDK classes

all SDK

Video

https://drive.google.com/file/d/1utr0448QR_-Qp8xJYFz5FjNnXyzx4yIo/view?usp=sharing

Other information

I use stripe test card 4000 0025 0000 3155

tjclawson-stripe commented 5 days ago

Hey @derynia. We recently upgraded the 3ds2 SDK and shipped in stripe-android version 21.2.0. Please upgrade to this version and confirm if the bug is still present.

derynia commented 5 days ago

Hey @derynia. We recently upgraded the 3ds2 SDK and shipped in stripe-android version 21.2.0. Please upgrade to this version and confirm if the bug is still present.

No it didn't go. Still the same. If I try to repeat a payment having a user secret I see screen 3ds check starting and then have a fail event as a response. Also in this case if I try to cancel the payment intent it doesn't succeed. But if I try to confirm intent again I get the normal 3ds check screen and able either cancel or confirm the intent.
I also changed the code this way:

@Composable
actual fun ThreeDSScreen(
    publishableKey: String,
    secret: String,
    onSuccess: () -> Unit,
    onCancel: () -> Unit,
    onFail: () -> Unit,
) {
    val paymentLauncher = rememberPaymentLauncher(
        publishableKey = publishableKey,
        stripeAccountId = null,
        callback = PaymentResultCallback(
            onSuccess = onSuccess,
            onCancel = onCancel,
            onFail = onFail
        )
    )
    LaunchedEffect(secret) {
        delay(300)
        paymentLauncher.confirm(
            ConfirmPaymentIntentParams.create(
                clientSecret = secret,
                paymentMethodType = PaymentMethod.Type.Card
            )
        )
    }
}

class PaymentResultCallback(
    private val onSuccess: () -> Unit,
    private val onCancel: () -> Unit,
    private val onFail: () -> Unit,
) : PaymentLauncher.PaymentResultCallback {
    override fun onPaymentResult(paymentResult: PaymentResult) {
        when (paymentResult) {
            is PaymentResult.Completed -> {
                onSuccess()
            }

            is PaymentResult.Canceled -> {
                onCancel()
            }

            is PaymentResult.Failed -> {
                onFail()
            }
        }
    }
}
davidme-stripe commented 5 days ago

@derynia Can you try one of these test cards? https://docs.stripe.com/testing?locale=en-GB#3d-secure-mobile-challenge-flows

The 3155 is a test card for a different scenario and may not work in the Mobile SDK: We'll try to make that clearer in the docs.

derynia commented 5 days ago

@derynia Can you try one of these test cards? https://docs.stripe.com/testing?locale=en-GB#3d-secure-mobile-challenge-flows

Used this one 4000582600000094

The 3ds screen is different but the behaviour is the same. The same issue. Also when using old card I could cancel intent from the second try when the screen appeared and I press close and send delete intent to our backed server which sends it to Stripe. But when using those cards I can't even cancel the pending intent when pressing Cancel on 3ds screen. I could do it when using previous cards.

I'd also want too point out that I'm using this all not on pure Android app but in KMP app. But the 3ds test screen runs normally when I'm making a payment intent and at once trying to run 3ds confirmation. But it doesn't work when I try to confirm payment when I'm already having client secret.

I thought maybe the problem is that I'm entering Composable function two times. But no I tested in debugger I enter it only one time.

As we're speaking of multiplatform app I have also iOS implementation which is also native. I'm using Stripe components for iOS. And the behaviour is the same. Everything works fine when I'm creating payment intent (for card I already saved or for a new card) and then confirm it at once, making sequencial requests it works. But when I'm trying to confirm payment intent using secret key that I already have it doesn't work. Also in iOS I see error 401 in log. LOG ANALYTICS: stripeios.paymenthandler.confirm.finished - [(key: "status", value: "failed"), (key: "payment_method_type", value: "card"), (key: "intent_id", value: "pi_3QOouCKYhPgWGtwt1msUF80D"), (key: "error_type", value: "com.stripe3ds2"), (key: "error_code", value: "401")]

I thought maybe a problem is that we have backend on our project which does main communication with Stripe. I make direct requests to Stripe only when saving a card, for creating a payment intent I make request to our backend which makes intent and returns me a secret. But if it was the problem I couldn't process any payment with 3ds at all.