Skip to content

[BUG] PaymentSheet card scanner: launch() on unregistered ActivityResultLauncher in CardScanGoogleLauncher #13160

Description

@orcuns

Summary

PaymentSheet's built-in Google card-recognition scanner (com.stripe.android.ui.core.cardscan)
crashes with IllegalStateException: Attempting to launch an unregistered ActivityResultLauncher
when the recognition IntentSender resolves after the launcher has been unregistered.

DefaultPaymentCardRecognitionClient.fetchIntent requests the card-recognition intent from
Google Play Services asynchronously. When the GMS Task completes on the main thread,
CardScanGoogleLauncher.launch() calls ManagedActivityResultLauncher.launch(). If the sheet /
composable left composition during that async gap (user dismissed it, navigated away, or a
config/lifecycle change occurred), the Compose-managed launcher is already unregistered and
launch() throws. The launch is not guarded by a registration/lifecycle check.

Full stack trace:

Fatal Exception: java.lang.IllegalStateException: Attempting to launch an unregistered ActivityResultLauncher with contract androidx.activity.result.contract.ActivityResultContracts$StartIntentSenderForResult and input ... You must ensure the ActivityResultLauncher is registered before calling launch().
at androidx.activity.result.ActivityResultRegistry$register$3.launch(ActivityResultRegistry.java:184)
at androidx.activity.compose.ActivityResultLauncherHolder.launch(ActivityResultLauncherHolder.java:150)
at androidx.activity.compose.ManagedActivityResultLauncher.launch(ActivityResultRegistry.kt:139)
at com.stripe.android.ui.core.cardscan.CardScanGoogleLauncher.launch$lambda$1(CardScanGoogleLauncher.kt:62)
at com.stripe.android.ui.core.cardscan.DefaultPaymentCardRecognitionClient.fetchIntent$lambda$0$0(DefaultPaymentCardRecognitionClient.kt:32)
at com.stripe.android.ui.core.cardscan.DefaultPaymentCardRecognitionClient.fetchIntent$lambda$0$1(DefaultPaymentCardRecognitionClient.kt:29)
at com.google.android.gms.tasks.zzm.run(zzm.java:1)

Expected: the async card-scan launch should be a no-op (or handled gracefully) when the
launcher/host is no longer registered/STARTED, instead of throwing.

Suggested fix: guard the launch() in CardScanGoogleLauncher — verify the host Lifecycle is at
least STARTED / the launcher is still registered before launching, or drop the GMS Task
continuation result if unregistration happened in the meantime.

Code to reproduce

We use the standard PaymentSheet integration (no stripecardscan dependency — this is purely the
built-in Google PaymentCardRecognition path):

val paymentSheet = PaymentSheet.Builder(paymentResultCallback).build()
paymentSheet.presentWithSetupIntent(setupIntentClientSecret, configuration)

Steps:

  1. Present PaymentSheet on a device whose Google Play Services supports PaymentCardRecognition
    (so the "Scan card" affordance appears on the add-card screen).
  2. Trigger the card scan, then immediately dismiss the sheet / navigate away before the GMS
    recognition Task resolves.
  3. When the Task continuation runs, CardScanGoogleLauncher.launch() fires on a now-unregistered
    launcher → crash.

Android version

Observed in production on modern Android (the crashing session is API 33+; lazy/loop frames in the
trace are consistent with Android 13/14). I can provide the exact OS-version breakdown from
Crashlytics on request. Not a regression from a specific OS update — it's a timing race.

Impacted devices

Not device-specific. Reproducible by inspection of the async flow on any device that surfaces the
Google card-recognition "Scan card" option.

Installation method

Gradle dependency via version catalog: com.stripe:stripe-android:23.2.0.

Dependency Versions

kotlin: 2.3.21
stripe-android: 23.2.0
Android Gradle Plugin: 9.2.1
Gradle: 9.4.1

SDK classes

  • com.stripe.android.ui.core.cardscan.CardScanGoogleLauncher
  • com.stripe.android.ui.core.cardscan.DefaultPaymentCardRecognitionClient
  • com.stripe.android.paymentsheet.PaymentSheet (payments-ui-core)

Video

Not available — production Crashlytics crash, not consistently reproducible on demand due to the
narrow timing window. Stack trace above pinpoints the faulty call site.

Other information

  • The faulty launch() is entirely inside the SDK and there is no public PaymentSheet config to
    disable the Google card-recognition scanner, so integrators have no workaround.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions