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:
- Present PaymentSheet on a device whose Google Play Services supports PaymentCardRecognition
(so the "Scan card" affordance appears on the add-card screen).
- Trigger the card scan, then immediately dismiss the sheet / navigate away before the GMS
recognition Task resolves.
- 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.
Summary
PaymentSheet's built-in Google card-recognition scanner (
com.stripe.android.ui.core.cardscan)crashes with
IllegalStateException: Attempting to launch an unregistered ActivityResultLauncherwhen the recognition IntentSender resolves after the launcher has been unregistered.
DefaultPaymentCardRecognitionClient.fetchIntentrequests the card-recognition intent fromGoogle Play Services asynchronously. When the GMS
Taskcompletes on the main thread,CardScanGoogleLauncher.launch()callsManagedActivityResultLauncher.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()inCardScanGoogleLauncher— verify the host Lifecycle is atleast 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
stripecardscandependency — this is purely thebuilt-in Google PaymentCardRecognition path):
Steps:
(so the "Scan card" affordance appears on the add-card screen).
recognition Task resolves.
CardScanGoogleLauncher.launch()fires on a now-unregisteredlauncher → 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
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
launch()is entirely inside the SDK and there is no public PaymentSheet config todisable the Google card-recognition scanner, so integrators have no workaround.