Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by EclipseNavigator145

Why does confirming a pending Stripe PaymentIntent for 3DS fail on first attempt in Kotlin Compose?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm handling a Stripe 3DS payment workflow in my Jetpack Compose app, but I'm running into an issue when re-launching a pending PaymentIntent.

I create a PaymentIntent with a new or saved card, then check if 3DS authentication is needed. If so, I initiate the 3DS process as shown below:

KOTLIN
@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() } } } }

I also save the client secret in encrypted shared preferences. The flow works as expected when the user cancels, fails, or confirms the 3DS payment.

However, if the app is stopped and then restarted, I check for an existing pending PaymentIntent and pass the stored client secret to the composable again. When testing with Stripe's test VISA card (which requires 3DS), the Stripe 3DS screen is launched but immediately the process fails as if the user pressed fail.

After this failure, I attempt to cancel the pending PaymentIntent and confirm it again, which then succeeds on the second attempt. It seems there is an issue with the initial confirmation post-app restart.

What am I doing wrong?

0

Comments 0

Answers 3

Views 68

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below is some guidance based on known patterns when confirming PaymentIntents with 3D Secure using the Stripe SDK:

1. Use the Correct Payment Method Type

Make sure you’re confirming a PaymentIntent with the proper payment method type. For online 3DS authentication you want to use the standard card type (i.e. "card"), not "card_present" (which is used for in‐person payments). Using the wrong type may cause the first attempt to fail and force you into a fallback flow.

2. Handle Pending PaymentIntents Properly

When your app restarts and you detect a pending PaymentIntent, instead of directly calling confirm with the saved client secret you need to first retrieve the PaymentIntent to check its current status. In manual confirmation mode, if actions have been taken (or attempted) already, the PaymentIntent may have reverted to a state (like requires_confirmation) that needs special handling. Re‑confirming a PaymentIntent that is in an intermediate state might be causing the failure on the first run.

3. Consider Timing and State

Because 3DS payments involve a browser redirect or an in‑app modal for authentication, a speedier re‑confirmation (for example, after only a short delay) might lead to a situation where the PaymentIntent state isn’t fully updated on Stripe’s side. Ensuring that you have fully handled the first redirect outcome (by retrieving the PaymentIntent) before attempting to reconfirm is critical.

Suggested Next Steps

  • Change the PaymentMethod Type: If you’re processing a 3DS payment for online cards, update your confirm call to use the proper type:

    KOTLIN
    paymentLauncher.confirm( ConfirmPaymentIntentParams.create( clientSecret = secret, paymentMethodType = PaymentMethod.Type.Card // Use "card" for online payments. ) )
  • Retrieve the PaymentIntent on App Resume: Instead of immediately calling confirm again with the stored client secret, call Stripe’s retrieve API (or use your backend) to get the PaymentIntent status. If it is in a state (like requires_confirmation) that means the previous attempt already ran, take the correct remediation steps (for example, canceling or creating a new PaymentIntent).

  • Verify the Flow: Ensure that your handling for manual confirmation flow is in line with Stripe’s guidelines (i.e. only calling confirm with a secret key and handling the returned state appropriately).

Following these steps should resolve the odd first‑attempt failure and allow a smooth 3DS authentication flow even when resuming from a pending state.

If issues persist after these changes, consider logging the PaymentIntent state before and after confirmation so you can track any state transitions that might be unexpected.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by GalacticTraveler685 3 months ago

0

It appears you are mistakenly indicating CardPresent (which is for use with Stripe Terminal and reading cards in person), instead of Card for online cards.

I would recommend you revise this line and retest everything:

SWIFT
- paymentMethodType = PaymentMethod.Type.CardPresent + paymentMethodType = PaymentMethod.Type.Card

No comments yet.

Answer by UranianKeeper980 3 months ago

0

It sounds like you're running into an issue where the 3D Secure (3DS) flow is prematurely failing when you try to confirm the payment intent after the app is restarted, even though the clientSecret and setup seem correct. The issue could be related to how the payment confirmation process is triggered after the app restarts, as well as how the payment state is handled in your app during the reinitialization.

Here's the code :
first pic of the code :
[1]: https://i.sstatic.net/nzkDItPN.png

No comments yet.

Discussion

No comments yet.