Skip to content

Add EmbeddedPaymentElement support for Flutter#2239

Merged
remonh87 merged 18 commits into
flutter-stripe:mainfrom
Quibli-Development:sync
Apr 28, 2026
Merged

Add EmbeddedPaymentElement support for Flutter#2239
remonh87 merged 18 commits into
flutter-stripe:mainfrom
Quibli-Development:sync

Conversation

@programmeraditya

@programmeraditya programmeraditya commented Oct 13, 2025

Copy link
Copy Markdown
Contributor

What This Adds

Implements the new Embedded Payment Element from Stripe's mobile SDKs. Lets you embed the payment UI inline instead of using modal sheets.

Changes

  • New EmbeddedPaymentElement widget with iOS/Android platform views
  • EmbeddedPaymentElementController to call confirm() and clearPaymentOption()
  • Callbacks for payment selection, height changes, loading failures, form sheet completion, and immediate actions
  • Works with both payment and setup intents

Status

iOS ✅

Works. Platform view renders properly, confirm() returns the result, all callbacks fire. Tested with cards, Apple Pay, Link, PayPal, Revolut Pay.

Android ✅

Works. Platform view renders properly, confirm() returns the result, all callbacks fire. Tested with cards, Google Pay, Link, PayPal, Revolut Pay.

Quick Example

EmbeddedPaymentElement(
  intentConfiguration: IntentConfiguration(
    mode: IntentMode.paymentMode(amount: 5000, currencyCode: 'USD'),
    confirmHandler: (paymentMethod, shouldSave, callback) async {
      // Get client secret from your backend
      callback(IntentCreationCallbackParams(clientSecret: secret));
    },
  ),
  configuration: SetupPaymentSheetParameters(
    merchantDisplayName: 'My Store',
    customerId: customerId,
    customerEphemeralKeySecret: ephemeralKey,
  ),
  controller: controller,
  onPaymentOptionChanged: (option) => print('Payment method selected'),
)

React Native docs: https://docs.stripe.com/payments/mobile/accept-payment-embedded?platform=react-native

Summary by CodeRabbit

  • New Features
    • Embedded Payment Element added for Flutter with native Android/iOS and React Native support
    • Controller to manage Embedded Payment Element lifecycle and actions (confirm, clear, dispose)
    • New API to register/update a confirm handler
    • Support for selecting a paymentMethodConfigurationId in intent/payment-sheet configs
    • Android: Jetpack Compose enabled for embedded element rendering
  • Stability
    • Improved loading/error reporting and height update handling for the embedded element

@jonasbark

jonasbark commented Oct 17, 2025

Copy link
Copy Markdown
Member

It looks really good upon first inspection, thanks for all the effort!

I was hesitant to add it due to the Compose requirement, but I think it's a transitive dependency of stripe-android anyway.

Will have a closer look soon

@remonh87 remonh87 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked it from the dart side and looks good to me only I think dart format did create a bigger change than needed

thank you for this great contribution. I will wait for @jonasbark feedback before integrating it

black,
automatic,
}
enum ApplePayButtonStyle { white, whiteOutline, black, automatic }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What command did you run for dart format?

Looks like quite some changes were made due formatter

Comment on lines +700 to +704
@override
void setConfirmHandler(ConfirmHandler? handler) {
_confirmHandler = handler;
}

@tara-pogancev tara-pogancev Oct 22, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When opening the code locally, I get an error for WebStripe class:
"Missing concrete implementation of 'abstract class StripePlatform extends PlatformInterface. setConfirmHandler'."

I fixed it via quick fix, but should this change for web package be in the PR as well?

@programmeraditya

programmeraditya commented Nov 24, 2025

Copy link
Copy Markdown
Contributor Author

Hello @remonh87
Is there any update on when this PR can be merged?

@remonh87

Copy link
Copy Markdown
Member

@jonasbark can you take a look at it as well?

@programmeraditya

Copy link
Copy Markdown
Contributor Author

Hello, it's been a few months since this PR was last approved. An update on when it will be merged would be greatly appreciated. @jonasbark @remonh87

@remonh87

remonh87 commented Mar 9, 2026

Copy link
Copy Markdown
Member

@programmeraditya yes we should merge it can you rebase the changes to the latest main ? I see there are a few merge conflicts. Let me know if you need help

@remonh87 remonh87 added the Awaiting response Awaiting response from the issuer label Mar 27, 2026
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a new Embedded Payment Element: Flutter widget + controller, platform-interface confirm-handler, Android/iOS platform views and native integrations, React Native Android view manager, model/serialization changes, and build/config updates across Flutter, Android, and iOS packages.

Changes

Cohort / File(s) Summary
Flutter public API
packages/stripe/lib/flutter_stripe.dart, packages/stripe/lib/src/stripe.dart
Exported EmbeddedPaymentElement widget and controller; added Stripe.setConfirmHandler(ConfirmHandler?).
Flutter widget & controller
packages/stripe/lib/src/widgets/embedded_payment_element.dart, packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart
New stateful widget, controller, typedefs, exception type, method-channel plumbing, confirm/clear APIs, lifecycle/attach-detach and dispose handling.
Platform interface & models
packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart, .../method_channel_stripe.dart, .../models/payment_sheet.*
Added StripePlatform.setConfirmHandler, platform override, new paymentMethodConfigurationId on IntentConfiguration, and explicitToJson on nested appearance/config types (adjusted generated code).
Android build & plugin
packages/stripe_android/android/build.gradle, .../gradle-wrapper.properties, packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt
Enabled Compose, updated Gradle wrapper settings, added Compose deps, and registered embedded payment element platform view factory.
Android platform view & utils
packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView*.kt, .../StripeSdkModuleExtensions.kt
New PlatformView and Factory, per-view MethodChannel handlers (events/confirm/clear), creation param parsing, and mapToBundle() helper for nested Map→Bundle conversion.
Android React Native integration
packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElement*.kt
Compose-based EmbeddedPaymentElementView, EmbeddedPaymentElementViewManager with props, parsing, command handlers (confirm/clear), event emission, and config threading.
iOS platform view & SDK integration
packages/stripe_ios/.../EmbeddedPaymentElementFactory.swift, .../EmbeddedPaymentElementView.swift, .../StripePlugin.swift, .../StripeSdkImpl+*.swift
Added iOS platform view factory and container view, per-view MethodChannel, height/payment-option delegates, structured loading error dispatch, paymentMethodConfigurationId threading, safer confirm handling, and merchantIdentifier propagation; removed older RN manager.
iOS dependency update
packages/stripe_ios/ios/stripe_ios/Package.resolved
Bumped stripe-ios-spm from 24.16.1 to 25.9.0.
Generated/Model updates
packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart, .../payment_sheet.g.dart
Propagated paymentMethodConfigurationId into generated APIs and adjusted nested object JSON serialization (explicitToJson).

Sequence Diagram

sequenceDiagram
    participant App as Flutter App
    participant Widget as EmbeddedPaymentElement
    participant Channel as MethodChannel
    participant Native as Platform View (Android/iOS)
    participant SDK as Stripe SDK

    App->>Widget: instantiate(intentConfiguration, configuration)
    Widget->>Channel: create platform view (creationParams)
    Channel->>Native: create view / open channel
    Native->>SDK: configure embedded element
    SDK-->>Native: ready / height & events
    Native-->>Channel: onHeightChanged / onPaymentOptionChanged / loadingFailed
    Channel-->>Widget: dispatch callbacks
    App->>Widget: call confirm()
    Widget->>Channel: confirm RPC
    Channel->>Native: confirm
    Native->>SDK: perform confirm
    SDK-->>Native: confirm result
    Native-->>Channel: return result map
    Channel-->>Widget: result
    Widget-->>App: deliver result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • Sync #2379 — Related additions of Embedded Payment Element platform views and confirm-handler plumbing across native and Flutter layers.
  • Sync #2336 — Related iOS Stripe SDK integration changes and package resolution updates affecting embedded element wiring.

Suggested reviewers

  • jonasbark
  • remonh87

"🐰
I hopped a view from native trees,
Brought heights, errors, and confirm keys,
Channels hum and callbacks sing,
Payments tuck their little wing,
Controllers hop — rejoice, please!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add EmbeddedPaymentElement support for Flutter' is directly related to the main changes in the changeset, which introduce a new EmbeddedPaymentElement widget and controller for Flutter with iOS and Android platform implementations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@programmeraditya

Copy link
Copy Markdown
Contributor Author

@remonh87 the code is up to date with main. Also, the ConfirmHandler function has been reverted to only take two arguments. Earlier this PR introduced a third argument which was intentCreationCallback, and instead of that now Stripe.instance.intentCreationCallback must be used inside the confirmHandler function.
thank you!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stripe_platform_interface/lib/src/models/payment_sheet.dart (1)

881-896: ⚠️ Potential issue | 🟠 Major

FlatConfig must also have @JsonSerializable(explicitToJson: true) to properly serialize nested configs.

When RowConfig.toJson() is called (which has explicitToJson: true), it invokes flat?.toJson(). However, FlatConfig.toJson() currently does not recursively serialize its nested custom objects (separatorInsets, radio, checkmark, disclosure). Without explicitToJson: true, the generated _$FlatConfigToJson assigns these as raw model objects rather than calling their .toJson() methods. Since StandardMessageCodec only accepts primitives, lists, and maps, the platform-view creation will fail when encoding row.flat.* customizations. Add @JsonSerializable(explicitToJson: true) to FlatConfig:

Suggested fix
 `@freezed`
 abstract class FlatConfig with _$FlatConfig {
+  `@JsonSerializable`(explicitToJson: true)
   const factory FlatConfig({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_platform_interface/lib/src/models/payment_sheet.dart` around
lines 881 - 896, The FlatConfig model lacks `@JsonSerializable`(explicitToJson:
true), so nested custom fields (separatorInsets, radio, checkmark, disclosure)
are not being serialized via their .toJson() methods; update the FlatConfig
class declaration to add the `@JsonSerializable`(explicitToJson: true) annotation
so the generated _$FlatConfigToJson will call .toJson() for those nested objects
(this aligns with how RowConfig is defined and fixes encoding for
RowConfig.toJson()).
🧹 Nitpick comments (4)
packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart (1)

199-200: Consider providing a default implementation to avoid breaking custom platform implementations.

Adding a new abstract method to StripePlatform will break any custom platform implementations. While this is likely acceptable for a beta feature, providing a default no-op implementation would be more defensive.

💡 Suggested default implementation
  /// Set the confirm handler for embedded payment elements
- void setConfirmHandler(ConfirmHandler? handler);
+ void setConfirmHandler(ConfirmHandler? handler) {
+   // Default no-op implementation for platforms that don't support embedded payment elements
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart`
around lines 199 - 200, The new abstract method
setConfirmHandler(ConfirmHandler? handler) on StripePlatform will break existing
custom platform implementations; change it to provide a default no-op
implementation on the StripePlatform base class instead of leaving it abstract:
add a concrete method setConfirmHandler(ConfirmHandler? handler) { /* no-op */ }
(or call a protected hook) so existing subclasses are not forced to implement
it, referencing the StripePlatform class and the setConfirmHandler and
ConfirmHandler symbols to locate where to add the default.
packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt (1)

85-103: List handling could silently drop nested objects/maps.

When a list contains nested Maps or other complex types, the function logs an error but doesn't convert them. This could lead to data loss if the API evolves to pass arrays of objects. Consider documenting this limitation or adding support for List<Map> if needed for future embedded payment element configurations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt`
around lines 85 - 103, The code currently logs and drops lists of complex
objects in mapToBundle (seen around result.putStringArrayList /
result.putIntegerArrayList and the android.util.Log.e("mapToBundle"...)), so
update mapToBundle to handle List<Map<*,*>> by converting each Map to a Bundle
recursively (reusing the same mapToBundle logic), collect them into an
ArrayList<Bundle> and call result.putParcelableArrayList(key, arrayList) instead
of logging an error; keep the existing string/int branches as-is and document
the remaining unsupported types if any.
packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties (1)

3-5: Consider upgrading Android Gradle Plugin (AGP) to 8.13+ for better alignment with Gradle 9.3.1.

Gradle 9.3.1 is compatible with AGP 8.1.3, but not officially tested together. Official recommendations suggest matching major versions (AGP 8.x with Gradle 8.x) for stability. AGP 8.x relies on internal Gradle APIs that may break in future Gradle 9.x releases. For long-term maintainability, either upgrade AGP to 8.13+ (tested with Gradle 9.x) or use Gradle 8.x instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties`
around lines 3 - 5, The project is using Gradle 9.3.1 (distributionUrl) but the
Android Gradle Plugin (com.android.tools.build:gradle) isn’t guaranteed
compatible; either upgrade the AGP to a 8.13+ release by updating the
com.android.tools.build:gradle dependency in your buildscript/classpath or
plugin block (e.g., bump to 8.13.x) to match Gradle 9.x, or instead pin the
wrapper’s distributionUrl to a Gradle 8.x release (replace gradle-9.3.1-bin.zip
with a Gradle 8.x distribution) so AGP 8.x remains supported—update the
classpath/plugin declaration for com.android.tools.build:gradle and/or the
distributionUrl in gradle-wrapper.properties accordingly.
packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart (1)

24-38: Consider throwing instead of assert for consistent debug/release behavior.

The assert followed by _context?.confirm() creates inconsistent behavior:

  • Debug mode: Assert fires, throws if not attached
  • Release mode: Assert stripped, silently returns null

For a payment flow, silent failures in release could mask bugs.

♻️ Option: Throw StateError for consistent behavior
   Future<Map<String, dynamic>?> confirm() async {
-    assert(
-      hasEmbeddedPaymentElement,
-      'Controller must be attached to an EmbeddedPaymentElement',
-    );
-    return await _context?.confirm();
+    final context = _context;
+    if (context == null) {
+      throw StateError('Controller must be attached to an EmbeddedPaymentElement');
+    }
+    return await context.confirm();
   }

   Future<void> clearPaymentOption() async {
-    assert(
-      hasEmbeddedPaymentElement,
-      'Controller must be attached to an EmbeddedPaymentElement',
-    );
-    await _context?.clearPaymentOption();
+    final context = _context;
+    if (context == null) {
+      throw StateError('Controller must be attached to an EmbeddedPaymentElement');
+    }
+    await context.clearPaymentOption();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart`
around lines 24 - 38, Replace the debug-only asserts in confirm() and
clearPaymentOption() with explicit runtime checks that throw a descriptive
StateError when hasEmbeddedPaymentElement is false so behavior is consistent in
release builds; specifically, in both methods (confirm and clearPaymentOption)
check hasEmbeddedPaymentElement and if false throw StateError('Controller must
be attached to an EmbeddedPaymentElement'), then call the non-null _context
methods (e.g., _context!.confirm() and _context!.clearPaymentOption()) to avoid
silent null returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stripe_android/android/build.gradle`:
- Around line 81-86: The Compose BOM version in the build.gradle (the
implementation platform('androidx.compose:compose-bom:2023.10.01') line used for
EmbeddedPaymentElement) is too old for Compose UI 1.10.x required by Stripe
Android SDK 23.x; update the BOM to a newer release (e.g., '2024.04.00' or
later) that provides Compose UI 1.10.x+, then sync/resolve dependencies and
verify the other Compose artifacts (androidx.compose.ui:ui, foundation, runtime,
activity-compose) are compatible with the upgraded BOM.
- Around line 48-50: Remove the legacy manual Compose compiler override: delete
the composeOptions block that sets kotlinCompilerExtensionVersion = '1.5.1' so
the applied plugin org.jetbrains.kotlin.plugin.compose can manage compiler
compatibility for Kotlin 2.1.0; ensure no other code references
kotlinCompilerExtensionVersion in this module and rely on the plugin-managed
Compose compiler version instead.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`:
- Around line 89-95: The confirm call can throw synchronously and leave the
Flutter result unresolved; wrap the call to viewManager.confirm(embeddedView) in
a try-catch, and in the catch call result.error(...) with a clear error
code/message (and include exception.message), also ensure you null out
embeddedView.onConfirmResult in both success and error paths so the callback
cannot leak; keep using embeddedView.onConfirmResult to deliver successful
result via result.success(resultMap) and clear it after.
- Around line 29-47: The callbacks on embeddedView (onHeightChanged,
onPaymentOptionChanged, onLoadingFailed, onRowSelectionImmediateAction,
onFormSheetConfirmComplete) call channel.invokeMethod(...) directly and must be
dispatched to the Android main/UI thread; update each lambda to post the
invokeMethod call to the main looper (e.g., using a
Handler(Looper.getMainLooper()).post { ... } or view.context.mainLooper/post
equivalent) so every channel.invokeMethod is executed on the UI thread while
preserving the existing payloads (height, paymentOption, error.toMap(), null,
result).

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`:
- Around line 117-135: If starting CustomPaymentMethodActivity or
requireStripeSdkModule() fails, send a terminal failed result so the payment
flow doesn't hang: in the first catch (around reactContext.startActivity(...))
call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(CustomPaymentMethodResult.failed())
after logging the exception; in the second catch (the IllegalArgumentException
from requireStripeSdkModule()) call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(CustomPaymentMethodResult.failed())
before invoking CustomPaymentMethodActivity.finishCurrent() and returning.
Ensure you reference CustomPaymentMethodActivity, requireStripeSdkModule(),
CustomPaymentMethodResult.failed(), and
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult() exactly as
named.
- Around line 138-186: The coroutine handling the JS confirm/resolution must
guarantee cleanup and avoid hanging: move KeepJsAwakeTask.stop() into a finally
block so it always runs (wrap the coroutine body that emits the event, awaits
stripeSdkModule.customPaymentMethodResultCallback, and handles the result in
try/finally), ensure CustomPaymentMethodActivity.finishCurrent() remains in its
outer finally, and add a timeout (e.g., withTimeout or withTimeoutOrNull) around
all awaits of JS Deferreds like
stripeSdkModule.customPaymentMethodResultCallback and
embeddedIntentCreationCallback so they can't suspend indefinitely; likewise, for
the lambda that resets embeddedIntentCreationCallback, wrap the await and the
embeddedIntentCreationCallback = null reset in a try/finally to always reset the
Deferred even if the await times out or throws.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`:
- Around line 184-220: The Android EmbeddedPaymentElement configuration never
reads the "returnURL" value from the input bundle, so add code to extract
bundle.getString("returnURL") (or bundle.getString("returnUrl") if that's the
key used elsewhere) and, if non-null and non-empty, call the corresponding
setter on the EmbeddedPaymentElement.Configuration.Builder (the
configurationBuilder variable created from
EmbeddedPaymentElement.Configuration.Builder) before build(); ensure the check
mirrors iOS behavior (treat empty as invalid) and place this logic alongside the
other optional setters like primaryButtonLabel/paymentMethodOrder so the return
URL is not silently dropped.
- Around line 130-148: Replace the fatal throws in the customer/appearance
parsing blocks so parse errors are propagated via the view's loading-failure
callback rather than throwing Error(); specifically, inside the try/catch around
buildCustomerConfiguration(...) (catching PaymentSheetException) and around
buildPaymentSheetAppearance(...) (catching PaymentSheetAppearanceException) call
the view.onLoadingFailed(...) (or the manager's equivalent used by
PaymentSheetManager) with a structured error that includes the caught exception
and context, and then return/abort configuration setup instead of rethrowing;
update EmbeddedPaymentElementViewManager's setConfiguration flow to mirror
PaymentSheetManager’s error path and preserve the original exception details.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`:
- Around line 123-145: The current implementation relies on the singleton
StripeSdkImpl.shared.embeddedInstance for EmbeddedPaymentElement (used in
createEmbeddedPaymentElement, attachEmbeddedView and callbacks like
confirm/clearPaymentOption), which allows a second view to overwrite the first
view's instance and misroute callbacks; change this to per-view instances by
keying instances and channels to a unique view identifier (e.g., viewId) instead
of using StripeSdkImpl.shared.embeddedInstance: create and store the
EmbeddedPaymentElement in a per-view registry (or attach it to the
view/controller object), route invokeMethod callbacks and
confirm/clearPaymentOption to the per-view instance from that registry, and
update createEmbeddedPaymentElement, attachEmbeddedView, and any confirm/clear
handlers to look up the instance by viewId rather than the shared singleton so
multiple EmbeddedPaymentElement views can coexist safely.
- Around line 190-192: The presenter assignment in attachEmbeddedView(_:) and
embeddedPaymentElementWillPresent(_:) currently uses
embeddedView.window?.rootViewController which can miss modals/pushed
controllers; replace those direct uses by calling the existing traversal helper
findViewControllerPresenter() (from StripeSdkImpl.swift) to obtain the topmost
view controller (following presentedViewController chain) and assign that to
embeddedElement.presentingViewController so presentation uses the correct
hierarchy.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe`
Sdk/EmbeddedPaymentElementView.swift:
- Around line 58-64: The updatePresentingViewController() method should use
StripeSdkImpl.findViewControllerPresenter() to locate the top-most presenter
instead of directly using window?.rootViewController; modify the block inside
DispatchQueue.main.async in updatePresentingViewController() to call
StripeSdkImpl.shared.findViewControllerPresenter() (or the equivalent public
helper) and assign that result to
StripeSdkImpl.shared.embeddedInstance?.presentingViewController, preserving the
weak self capture and main-thread dispatch so presentation uses the correct
top-most view controller across modal/navigation stacks.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swift`:
- Around line 841-846: The call to
StripeSdkImpl.shared.intentCreationCallback(...) unconditionally routes Dart
callbacks through the singleton, which breaks instance-backed PaymentSheet
confirm flows that set self.paymentSheetIntentCreationCallback in
buildIntentConfiguration(...); change this site to prefer the instance callback:
if the instance (self) has the pending paymentSheetIntentCreationCallback /
paymentSheetConfirmationTokenIntentCreationCallback, call
self.intentCreationCallback(result:resolver:rejecter:), otherwise fall back to
StripeSdkImpl.shared.intentCreationCallback(...), preserving the existing
resolver/rejecter creation and the trailing result(nil) behavior.

In
`@packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart`:
- Around line 8785-8788: The _FlatConfig class annotation is missing
explicitToJson: true which causes nested fields (separatorInsets, radio,
checkmark, disclosure) to be serialized as raw maps instead of calling their
.toJson() methods; update the `@JsonSerializable`() on the _FlatConfig declaration
to `@JsonSerializable`(explicitToJson: true) and then regenerate the generated
part file (payment_sheet.g.dart) so the generated _$FlatConfigToJson() calls
.toJson() for those nested properties.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart`:
- Around line 120-124: didUpdateWidget currently only updates the controller and
leaves native config stale because creationParams aren’t applied after initial
platform view creation; update didUpdateWidget in EmbeddedPaymentElement to
detect changes to intentConfiguration, configuration, and confirmHandler and
propagate them to the native element via the platform channel (invokeMethod)
similar to CardField, sending the new creationParams or specific keys to update
the live native instance; also change dispose to only clear the global confirm
handler (Stripe.instance.setConfirmHandler(null)) if the handler being removed
is the same one that this instance originally set (track the currently set
handler on the instance) to avoid wiping a handler used by another live element.

---

Outside diff comments:
In `@packages/stripe_platform_interface/lib/src/models/payment_sheet.dart`:
- Around line 881-896: The FlatConfig model lacks
`@JsonSerializable`(explicitToJson: true), so nested custom fields
(separatorInsets, radio, checkmark, disclosure) are not being serialized via
their .toJson() methods; update the FlatConfig class declaration to add the
`@JsonSerializable`(explicitToJson: true) annotation so the generated
_$FlatConfigToJson will call .toJson() for those nested objects (this aligns
with how RowConfig is defined and fixes encoding for RowConfig.toJson()).

---

Nitpick comments:
In `@packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties`:
- Around line 3-5: The project is using Gradle 9.3.1 (distributionUrl) but the
Android Gradle Plugin (com.android.tools.build:gradle) isn’t guaranteed
compatible; either upgrade the AGP to a 8.13+ release by updating the
com.android.tools.build:gradle dependency in your buildscript/classpath or
plugin block (e.g., bump to 8.13.x) to match Gradle 9.x, or instead pin the
wrapper’s distributionUrl to a Gradle 8.x release (replace gradle-9.3.1-bin.zip
with a Gradle 8.x distribution) so AGP 8.x remains supported—update the
classpath/plugin declaration for com.android.tools.build:gradle and/or the
distributionUrl in gradle-wrapper.properties accordingly.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt`:
- Around line 85-103: The code currently logs and drops lists of complex objects
in mapToBundle (seen around result.putStringArrayList /
result.putIntegerArrayList and the android.util.Log.e("mapToBundle"...)), so
update mapToBundle to handle List<Map<*,*>> by converting each Map to a Bundle
recursively (reusing the same mapToBundle logic), collect them into an
ArrayList<Bundle> and call result.putParcelableArrayList(key, arrayList) instead
of logging an error; keep the existing string/int branches as-is and document
the remaining unsupported types if any.

In `@packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart`:
- Around line 199-200: The new abstract method setConfirmHandler(ConfirmHandler?
handler) on StripePlatform will break existing custom platform implementations;
change it to provide a default no-op implementation on the StripePlatform base
class instead of leaving it abstract: add a concrete method
setConfirmHandler(ConfirmHandler? handler) { /* no-op */ } (or call a protected
hook) so existing subclasses are not forced to implement it, referencing the
StripePlatform class and the setConfirmHandler and ConfirmHandler symbols to
locate where to add the default.

In `@packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart`:
- Around line 24-38: Replace the debug-only asserts in confirm() and
clearPaymentOption() with explicit runtime checks that throw a descriptive
StateError when hasEmbeddedPaymentElement is false so behavior is consistent in
release builds; specifically, in both methods (confirm and clearPaymentOption)
check hasEmbeddedPaymentElement and if false throw StateError('Controller must
be attached to an EmbeddedPaymentElement'), then call the non-null _context
methods (e.g., _context!.confirm() and _context!.clearPaymentOption()) to avoid
silent null returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc6af007-7300-45e2-bb63-285a9fce7967

📥 Commits

Reviewing files that changed from the base of the PR and between 4e221d6 and 3bf238e.

📒 Files selected for processing (24)
  • packages/stripe/lib/flutter_stripe.dart
  • packages/stripe/lib/src/stripe.dart
  • packages/stripe/lib/src/widgets/embedded_payment_element.dart
  • packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart
  • packages/stripe_android/android/build.gradle
  • packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformViewFactory.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt
  • packages/stripe_ios/ios/stripe_ios/Package.resolved
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/EmbeddedPaymentElementView.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+Embedded.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+PaymentSheet.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swift
  • packages/stripe_platform_interface/lib/src/method_channel_stripe.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart
  • packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart

Comment on lines +48 to +50
composeOptions {
kotlinCompilerExtensionVersion = '1.5.1'
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/stripe_android/android/build.gradle | head -100

Repository: flutter-stripe/flutter_stripe

Length of output: 3470


🏁 Script executed:

rg -n "kotlin_version|composeOptions|org.jetbrains.kotlin.plugin.compose" packages/stripe_android/android/build.gradle

Repository: flutter-stripe/flutter_stripe

Length of output: 358


🌐 Web query:

Compose Compiler 1.5.1 Kotlin compatibility requirements version

💡 Result:

The Compose Compiler version 1.5.1 is compatible with Kotlin version 1.9.0.

Citations:


🌐 Web query:

Jetpack Compose Kotlin 2.0 2.1.0 composeOptions kotlinCompilerExtensionVersion

💡 Result:

For Kotlin 2.0 or 2.1.0+, do not use composeOptions.kotlinCompilerExtensionVersion. Instead, apply the Compose Compiler Gradle plugin "org.jetbrains.kotlin.plugin.compose" with version matching your Kotlin version (e.g., 2.1.0). This plugin manages the Compose compiler automatically, making it compatible without manual version specification in composeOptions. Legacy setup (Kotlin <2.0): Use composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } for Kotlin 1.9.25, per compatibility map. But for Kotlin 2.0+, the plugin approach is required and recommended. Current Jetpack Compose libraries (March 2026, BOM 2026.03.00): Stable versions are 1.10.6 (animation, foundation, material, runtime, ui), 1.4.0 (material3). Use BOM for dependency management: platform("androidx.compose:compose-bom:2026.03.00"). No "2.1.0" or "2.0" Compose library versions exist; versioning is per-group around 1.10.x.

Citations:


Remove the legacy Compose compiler override.

This module uses Kotlin 2.1.0 and applies the Compose Compiler Gradle plugin (org.jetbrains.kotlin.plugin.compose). For Kotlin 2.0+, the plugin automatically manages Compose compiler compatibility—manual composeOptions override should not be used. Compose Compiler 1.5.1 is only compatible with Kotlin 1.9.0, making this override incompatible with your Kotlin version and causing version mismatch issues on certain toolchains.

Suggested fix
-    composeOptions {
-        kotlinCompilerExtensionVersion = '1.5.1'
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
composeOptions {
kotlinCompilerExtensionVersion = '1.5.1'
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_android/android/build.gradle` around lines 48 - 50, Remove
the legacy manual Compose compiler override: delete the composeOptions block
that sets kotlinCompilerExtensionVersion = '1.5.1' so the applied plugin
org.jetbrains.kotlin.plugin.compose can manage compiler compatibility for Kotlin
2.1.0; ensure no other code references kotlinCompilerExtensionVersion in this
module and rely on the plugin-managed Compose compiler version instead.

Comment on lines +81 to +86
// Jetpack Compose dependencies for EmbeddedPaymentElement
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.runtime:runtime'
implementation 'androidx.activity:activity-compose:1.8.0'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual build.gradle file in the repository
fd -e build.gradle | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🌐 Web query:

Stripe Android SDK 23.x Compose version requirements official documentation

💡 Result:

The official Stripe Android SDK documentation specifies Jetpack Compose version requirements for compatibility with SDK versions 23.x. Stripe Android SDK 23.0.0 and later (up to current version 23.1.0 as of March 2026) requires Jetpack Compose UI 1.10.x. This is stated in the SDK's README on GitHub and confirmed in compatibility tables. The SDK uses Jetpack Compose internally and updates dependencies monthly for broad ecosystem compatibility. Apps using Stripe Android SDK 23.x must align their Compose UI version to 1.10.x to avoid issues. General requirements for SDK 23.x: - Android 6.0+ (API 23+) - compileSdkVersion and targetSdkVersion 36 - Android Gradle Plugin 8.13.2+ - Gradle 9.3.1+ Official sources: Stripe Android SDK GitHub README and related docs.

Citations:


🏁 Script executed:

# Search for stripe-android dependencies in the repository
find . -type f -name "*.gradle" -o -name "*.gradle.kts" | xargs grep -l "stripe" 2>/dev/null | head -10

Repository: flutter-stripe/flutter_stripe

Length of output: 204


🌐 Web query:

androidx.compose.compose-bom 2023.10.01 Compose UI version mapping

💡 Result:

The official Android Developers documentation provides a BOM to library version mapping table at https://developer.android.com/develop/ui/compose/bom/bom-mapping, which includes androidx.compose.compose-bom version 2023.10.01. Users can select this BOM version from a dropdown to view the exact mapped versions for all Compose libraries, including Compose UI (androidx.compose.ui group). From the snippet extracts, for BOM 2023.10.01, Compose UI Group maps to version 1.5.4 (inferred from sequence matching newer BOMs like 2023.12.00 using 1.5.4 and progression from 1.3.0 in older BOMs to 1.10.x in 2026 BOMs). The BOM was released on October 18, 2023, as confirmed by Maven repository.

Citations:


🏁 Script executed:

# Check the actual build.gradle content
cat -n packages/stripe_android/android/build.gradle | sed -n '75,95p'

Repository: flutter-stripe/flutter_stripe

Length of output: 1177


🌐 Web query:

Jetpack Compose BOM 2023.10.01 which Compose UI version does it provide

💡 Result:

The Jetpack Compose BOM 2023.10.01 provides Compose UI version 1.9.4.

Citations:


Update the Compose BOM to a version supporting Compose UI 1.10.x.

Stripe Android SDK 23.x requires Jetpack Compose UI 1.10.x. The BOM 2023.10.01 provides Compose UI 1.9.4, which is incompatible. Use a newer BOM version (e.g., 2024.04.00 or later) that provides Compose UI 1.10.x or higher to avoid dependency resolution and runtime ABI issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_android/android/build.gradle` around lines 81 - 86, The
Compose BOM version in the build.gradle (the implementation
platform('androidx.compose:compose-bom:2023.10.01') line used for
EmbeddedPaymentElement) is too old for Compose UI 1.10.x required by Stripe
Android SDK 23.x; update the BOM to a newer release (e.g., '2024.04.00' or
later) that provides Compose UI 1.10.x+, then sync/resolve dependencies and
verify the other Compose artifacts (androidx.compose.ui:ui, foundation, runtime,
activity-compose) are compatible with the upgraded BOM.

Comment on lines +29 to +47
embeddedView.onHeightChanged = { height ->
channel.invokeMethod("onHeightChanged", mapOf("height" to height.toDouble()))
}

embeddedView.onPaymentOptionChanged = { paymentOption ->
channel.invokeMethod("onPaymentOptionChanged", mapOf("paymentOption" to paymentOption))
}

embeddedView.onLoadingFailed = { error: EmbeddedPaymentElementLoadingError ->
channel.invokeMethod("embeddedPaymentElementLoadingFailed", error.toMap())
}

embeddedView.onRowSelectionImmediateAction = {
channel.invokeMethod("embeddedPaymentElementRowSelectionImmediateAction", null)
}

embeddedView.onFormSheetConfirmComplete = { result ->
channel.invokeMethod("embeddedPaymentElementFormSheetConfirmComplete", result)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if EmbeddedPaymentElementView callbacks are documented to run on main thread
# Look for any threading documentation or runOnUiThread patterns in similar implementations
rg -n -C3 "onHeightChanged|onPaymentOptionChanged|onLoadingFailed" --type=kt

Repository: flutter-stripe/flutter_stripe

Length of output: 99


🏁 Script executed:

fd -e kt | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 1999


🏁 Script executed:

cat -n packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 5211


🏁 Script executed:

rg -n "onHeightChanged|onPaymentOptionChanged|onLoadingFailed" packages/stripe_android/

Repository: flutter-stripe/flutter_stripe

Length of output: 2048


🏁 Script executed:

rg -n "EmbeddedPaymentElementView\|channel\.invokeMethod" packages/stripe_android/ -A2 -B2

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

rg -n "runOnUiThread\|MainThread\|withContext.*Main" packages/stripe_android/

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

cat -n packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt | sed -n '290,310p;320,330p;380,395p'

Repository: flutter-stripe/flutter_stripe

Length of output: 2138


🏁 Script executed:

rg -n "onHeightChanged\|onPaymentOptionChanged\|onLoadingFailed\|onRowSelectionImmediateAction\|onFormSheetConfirmComplete" packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt -B5 -A2

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🌐 Web query:

Flutter MethodChannel.invokeMethod thread safety requirements

💡 Result:

Flutter's MethodChannel.invokeMethod has specific thread safety requirements for reliable operation. Dart/Flutter side (calling invokeMethod): - Can be called from the main UI isolate or registered background isolates. - Runs on Dart's event loop in the isolate; since Dart is single-threaded per isolate, no additional synchronization needed within an isolate. - For background isolates, use BackgroundIsolateBinaryMessenger to register the isolate first. Native platform side: - When Flutter calls native (invokeMethod): The handler (onMethodCall) executes on the platform's main/UI thread by default (Android main thread, iOS main thread). Use TaskQueue for background thread execution. - When native calls back to Flutter (channel.invokeMethod): Must be invoked from the platform's main/UI thread. Use runOnUiThread (Android) or DispatchQueue.main.async (iOS) if from background. - Results can be returned asynchronously from any thread. Best practices: - Avoid blocking operations in handlers/invocations to prevent UI jank. - Offload heavy work to background threads, return results on main thread. - MethodChannel guarantees FIFO ordering, so no reentrancy issues if following thread rules. No explicit mutex/locks needed if following these rules, as channels are serialized per channel.

Citations:


Dispatch channel.invokeMethod calls to the main thread.

The callbacks invoke channel.invokeMethod() directly. Per Flutter's MethodChannel documentation, invokeMethod() must be called from the platform's main/UI thread. If the underlying EmbeddedPaymentElementView fires these callbacks from a background thread (particularly within coroutines or asynchronous operations), this will violate Flutter's threading contract. Wrap the channel.invokeMethod() calls in a main thread dispatch, e.g., using context.post { channel.invokeMethod(...) } or a Handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`
around lines 29 - 47, The callbacks on embeddedView (onHeightChanged,
onPaymentOptionChanged, onLoadingFailed, onRowSelectionImmediateAction,
onFormSheetConfirmComplete) call channel.invokeMethod(...) directly and must be
dispatched to the Android main/UI thread; update each lambda to post the
invokeMethod call to the main looper (e.g., using a
Handler(Looper.getMainLooper()).post { ... } or view.context.mainLooper/post
equivalent) so every channel.invokeMethod is executed on the UI thread while
preserving the existing payloads (height, paymentOption, error.toMap(), null,
result).

Comment on lines +89 to +95
"confirm" -> {
embeddedView.onConfirmResult = { resultMap ->
result.success(resultMap)
embeddedView.onConfirmResult = null
}
viewManager.confirm(embeddedView)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for confirm() to prevent hanging Flutter side.

If viewManager.confirm(embeddedView) throws synchronously, the result callback is never completed, causing the Flutter side to hang indefinitely.

🔧 Proposed fix: Wrap in try-catch
             "confirm" -> {
                 embeddedView.onConfirmResult = { resultMap ->
                     result.success(resultMap)
                     embeddedView.onConfirmResult = null
                 }
-                viewManager.confirm(embeddedView)
+                try {
+                    viewManager.confirm(embeddedView)
+                } catch (e: Exception) {
+                    embeddedView.onConfirmResult = null
+                    result.error("confirm_failed", e.message, null)
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`
around lines 89 - 95, The confirm call can throw synchronously and leave the
Flutter result unresolved; wrap the call to viewManager.confirm(embeddedView) in
a try-catch, and in the catch call result.error(...) with a clear error
code/message (and include exception.message), also ensure you null out
embeddedView.onConfirmResult in both success and error paths so the callback
cannot leak; keep using embeddedView.onConfirmResult to deliver successful
result via result.success(resultMap) and clear it after.

Comment on lines +117 to +135
try {
val intent =
Intent(reactContext, CustomPaymentMethodActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
}
reactContext.startActivity(intent)
} catch (e: Exception) {
Log.e("StripeReactNative", "Failed to start CustomPaymentMethodActivity", e)
}

val stripeSdkModule =
try {
requireStripeSdkModule()
} catch (ex: IllegalArgumentException) {
Log.e("StripeReactNative", "StripeSdkModule not found for CPM callback", ex)
CustomPaymentMethodActivity.finishCurrent()
return@remember
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk && \
ls -la | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 1469


🏁 Script executed:

wc -l packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 179


🏁 Script executed:

cat -n packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 20397


🏁 Script executed:

cat -n packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CustomPaymentMethodActivity.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 3219


🏁 Script executed:

rg "CustomPaymentMethodResult" packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/ -A 3 -B 3

Repository: flutter-stripe/flutter_stripe

Length of output: 8530


Send a failed CustomPaymentMethodResult when setup fails.

If CustomPaymentMethodActivity fails to start or requireStripeSdkModule() throws, the callback returns without sending any result to the Stripe SDK. Since this callback is fire-and-forget and the coroutine (lines 142-186) is the only path that invokes CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(), the payment flow will hang indefinitely waiting for a completion that never arrives.

Both failure paths need to send a terminal result:

  • Line 123: If startActivity() fails, send CustomPaymentMethodResult.failed()
  • Line 134: If requireStripeSdkModule() fails, send CustomPaymentMethodResult.failed() instead of just returning
🧰 Tools
🪛 detekt (1.23.8)

[warning] 133-133: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 117 - 135, If starting CustomPaymentMethodActivity or
requireStripeSdkModule() fails, send a terminal failed result so the payment
flow doesn't hang: in the first catch (around reactContext.startActivity(...))
call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(CustomPaymentMethodResult.failed())
after logging the exception; in the second catch (the IllegalArgumentException
from requireStripeSdkModule()) call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(CustomPaymentMethodResult.failed())
before invoking CustomPaymentMethodActivity.finishCurrent() and returning.
Ensure you reference CustomPaymentMethodActivity, requireStripeSdkModule(),
CustomPaymentMethodResult.failed(), and
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult() exactly as
named.

Comment on lines +190 to +192
if let viewController = embeddedView.window?.rootViewController {
embeddedElement.presentingViewController = viewController
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the EmbeddedPaymentElementFactory.swift file
find . -name "EmbeddedPaymentElementFactory.swift" -type f

Repository: flutter-stripe/flutter_stripe

Length of output: 164


🏁 Script executed:

# Read the entire file to understand context
wc -l ./packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift

Repository: flutter-stripe/flutter_stripe

Length of output: 168


🏁 Script executed:

# Read the entire file with line numbers
cat -n ./packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift

Repository: flutter-stripe/flutter_stripe

Length of output: 11958


🏁 Script executed:

# Check if there are any other usages of presentingViewController in the codebase
rg "presentingViewController" --type swift -B 2 -A 2

Repository: flutter-stripe/flutter_stripe

Length of output: 9039


🏁 Script executed:

# Check the findViewControllerPresenter function more closely
sed -n '200,230p' ./packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe\ Sdk/StripeSdkImpl.swift

Repository: flutter-stripe/flutter_stripe

Length of output: 1760


🏁 Script executed:

# Find the findViewControllerPresenter function
rg "func findViewControllerPresenter" -A 15

Repository: flutter-stripe/flutter_stripe

Length of output: 2230


🏁 Script executed:

# Check EmbeddedPaymentElementView.swift to see the similar pattern there
rg "window\?.rootViewController" -B 3 -A 3

Repository: flutter-stripe/flutter_stripe

Length of output: 3992


Use proper view controller hierarchy traversal instead of only rootViewController.

The code at these locations directly assigns window?.rootViewController as the presenter, but this doesn't account for modals or pushed navigation controllers in the hierarchy. The codebase already has a findViewControllerPresenter() function in StripeSdkImpl.swift that correctly traverses through presentedViewController to find the topmost controller—use that pattern here instead. Without this traversal, authentication flows and form-sheet presentations will originate from the wrong hierarchy.

Locations needing fixes:
  • Line 190-192: attachEmbeddedView(_:) method
  • Line 248-250: embeddedPaymentElementWillPresent(_:) method
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`
around lines 190 - 192, The presenter assignment in attachEmbeddedView(_:) and
embeddedPaymentElementWillPresent(_:) currently uses
embeddedView.window?.rootViewController which can miss modals/pushed
controllers; replace those direct uses by calling the existing traversal helper
findViewControllerPresenter() (from StripeSdkImpl.swift) to obtain the topmost
view controller (following presentedViewController chain) and assign that to
embeddedElement.presentingViewController so presentation uses the correct
hierarchy.

Comment on lines 58 to 64
private func updatePresentingViewController() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
StripeSdkImpl.shared.embeddedInstance?.presentingViewController = RCTPresentedViewController()
if let viewController = self.window?.rootViewController {
StripeSdkImpl.shared.embeddedInstance?.presentingViewController = viewController
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "EmbeddedPaymentElementView.swift" 2>/dev/null | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

cd /tmp && find . -type f -name "*EmbeddedPaymentElement*" 2>/dev/null | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

# Search for the file in common locations
find . -path "*/stripe_ios/*EmbeddedPaymentElementView.swift" 2>/dev/null

Repository: flutter-stripe/flutter_stripe

Length of output: 172


🏁 Script executed:

ls -la /

Repository: flutter-stripe/flutter_stripe

Length of output: 830


🏁 Script executed:

# Try to find any swift files related to Stripe
find . -type f -name "*.swift" 2>/dev/null | grep -i embedded | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 360


🏁 Script executed:

# Check current working directory
pwd && ls -la

Repository: flutter-stripe/flutter_stripe

Length of output: 1274


🏁 Script executed:

find / -type f -name "EmbeddedPaymentElementView.swift" 2>/dev/null | head -5

Repository: flutter-stripe/flutter_stripe

Length of output: 189


🏁 Script executed:

# List available directories
ls -la /workspace 2>/dev/null || ls -la /repo 2>/dev/null || ls -la ~ 2>/dev/null || echo "Checking root for repo"

Repository: flutter-stripe/flutter_stripe

Length of output: 651


🏁 Script executed:

git ls-files | grep -i "EmbeddedPaymentElementView" | head -20

Repository: flutter-stripe/flutter_stripe

Length of output: 638


🏁 Script executed:

# Check if we're in a git repo
git rev-parse --show-toplevel 2>/dev/null || echo "Not in a git repository"

Repository: flutter-stripe/flutter_stripe

Length of output: 91


🏁 Script executed:

# List all files to understand structure
git ls-files | head -30

Repository: flutter-stripe/flutter_stripe

Length of output: 821


🏁 Script executed:

# Search for usage of presentingViewController
rg "presentingViewController" --type swift 2>/dev/null | head -50

Repository: flutter-stripe/flutter_stripe

Length of output: 2785


🏁 Script executed:

# Search for window?.rootViewController usage pattern
rg "window\?" --type swift 2>/dev/null | head -30

Repository: flutter-stripe/flutter_stripe

Length of output: 922


🏁 Script executed:

cat -n "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/EmbeddedPaymentElementView.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 2665


🏁 Script executed:

rg -A 15 "var presentingViewController:" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 665


🏁 Script executed:

rg -B 5 -A 10 "while let presented = presentingViewController.presentedViewController" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 774


🏁 Script executed:

# Check the full context of StripeSdkImpl.swift around presentingViewController logic
rg -B 10 -A 15 "return presentingViewController" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 1110


🏁 Script executed:

# Check EmbeddedPaymentElementFactory.swift to see how it handles presentingViewController
cat -n "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 11958


🏁 Script executed:

rg -B 5 -A 5 "window\?.rootViewController" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift" | head -40

Repository: flutter-stripe/flutter_stripe

Length of output: 778


🏁 Script executed:

# Check for any TODOs or FIXMEs related to this
rg -i "todo|fixme|hack" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/EmbeddedPaymentElementView.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

# Check the broader context of how presentingViewController is used in the Stripe SDK
rg -B 3 -A 3 "findViewControllerPresenter" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift"

Repository: flutter-stripe/flutter_stripe

Length of output: 4462


🏁 Script executed:

rg "RCTPresentedViewController" "packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+Embedded.swift" -B 5 -A 5

Repository: flutter-stripe/flutter_stripe

Length of output: 595


🏁 Script executed:

# Check if RCTPresentedViewController uses findViewControllerPresenter internally
fd "RCTPresentedViewController" -x cat {}

Repository: flutter-stripe/flutter_stripe

Length of output: 241


Use findViewControllerPresenter() instead of directly accessing window?.rootViewController.

The codebase has an established pattern in StripeSdkImpl.findViewControllerPresenter() that properly traverses the presented view controller hierarchy to find the top-most controller. This is critical because window?.rootViewController alone may not be the active presenter in modal or navigation stack scenarios. Using the direct window root can cause the embedded element to present sheets from the wrong controller. Apply the same traversal pattern used throughout StripeSdkImpl.swift for other presentation operations (payment sheet, Apple Pay, financial connections, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe`
Sdk/EmbeddedPaymentElementView.swift around lines 58 - 64, The
updatePresentingViewController() method should use
StripeSdkImpl.findViewControllerPresenter() to locate the top-most presenter
instead of directly using window?.rootViewController; modify the block inside
DispatchQueue.main.async in updatePresentingViewController() to call
StripeSdkImpl.shared.findViewControllerPresenter() (or the equivalent public
helper) and assign that result to
StripeSdkImpl.shared.embeddedInstance?.presentingViewController, preserving the
weak self capture and main-thread dispatch so presentation uses the correct
top-most view controller across modal/navigation stacks.

Comment on lines +841 to 846
StripeSdkImpl.shared.intentCreationCallback(
result: params,
resolver: resolver(for: result),
rejecter: rejecter(for: result)
)
result(nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Keep the instance-backed callback path for regular PaymentSheet.

initPaymentSheet(...) still builds IntentConfiguration on self, and buildIntentConfiguration(...) stores the pending paymentSheetIntentCreationCallback on that instance. Unconditionally routing the Dart callback through StripeSdkImpl.shared means the existing PaymentSheet confirm-handler flow can no longer reach the closure it registered, so IntentConfiguration.confirmHandler can hang after Dart returns the client secret.

Verify where the pending callback is stored. Expected: if it is an instance property set from buildIntentConfiguration(...), this call site needs a self fallback instead of always using StripeSdkImpl.shared.

#!/bin/bash
set -euo pipefail

rg -n -C3 'paymentSheetIntentCreationCallback|paymentSheetConfirmationTokenIntentCreationCallback|func intentCreationCallback' packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swift`
around lines 841 - 846, The call to
StripeSdkImpl.shared.intentCreationCallback(...) unconditionally routes Dart
callbacks through the singleton, which breaks instance-backed PaymentSheet
confirm flows that set self.paymentSheetIntentCreationCallback in
buildIntentConfiguration(...); change this site to prefer the instance callback:
if the instance (self) has the pending paymentSheetIntentCreationCallback /
paymentSheetConfirmationTokenIntentCreationCallback, call
self.intentCreationCallback(result:resolver:rejecter:), otherwise fall back to
StripeSdkImpl.shared.intentCreationCallback(...), preserving the existing
resolver/rejecter creation and the trailing result(nil) behavior.

Comment on lines +8785 to 8788
@JsonSerializable(explicitToJson: true)
class _RowConfig implements RowConfig {
const _RowConfig({this.style, this.additionalInsets, this.flat, this.floating});
factory _RowConfig.fromJson(Map<String, dynamic> json) => _$RowConfigFromJson(json);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

GFILE="$(fd -a '^payment_sheet\.g\.dart$' | head -n1)"
echo "Inspecting: $GFILE"

# Verify whether nested fields are serialized with ?.toJson() (expected)
rg -n "Map<String, dynamic> _\\$FlatConfigToJson|separatorInsets|radio|checkmark|disclosure|Map<String, dynamic> _\\$RowConfigToJson|Map<String, dynamic> _\\$EmbeddedPaymentElementAppearanceToJson" "$GFILE"

Repository: flutter-stripe/flutter_stripe

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

GFILE="$(fd -a '^payment_sheet\.g\.dart$' | head -n1)"
echo "Inspecting: $GFILE"
echo ""

# Find and extract the _$FlatConfigToJson function
echo "=== _\$FlatConfigToJson implementation ==="
rg -A 20 "^Map<String, dynamic> _\\\$FlatConfigToJson" "$GFILE" || echo "Function not found"
echo ""

# Find and extract the _$RowConfigToJson function
echo "=== _\$RowConfigToJson implementation ==="
rg -A 15 "^Map<String, dynamic> _\\\$RowConfigToJson" "$GFILE" || echo "Function not found"
echo ""

# Find and extract the _$EmbeddedPaymentElementAppearanceToJson function
echo "=== _\$EmbeddedPaymentElementAppearanceToJson implementation ==="
rg -A 20 "^Map<String, dynamic> _\\\$EmbeddedPaymentElementAppearanceToJson" "$GFILE" || echo "Function not found"

Repository: flutter-stripe/flutter_stripe

Length of output: 2660


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FFILE="$(fd -a '^payment_sheet\.freezed\.dart$' | head -n1)"
echo "Inspecting source: $FFILE"
echo ""

# Search for _FlatConfig class definition and its annotation
echo "=== _FlatConfig class and annotation ==="
rg -B 5 "class _FlatConfig implements FlatConfig" "$FFILE" | head -n 20

echo ""
echo "=== _RowConfig class and annotation ==="
rg -B 5 "class _RowConfig implements RowConfig" "$FFILE" | head -n 20

echo ""
echo "=== _EmbeddedPaymentElementAppearance class and annotation ==="
rg -B 5 "class _EmbeddedPaymentElementAppearance implements EmbeddedPaymentElementAppearance" "$FFILE" | head -n 20

Repository: flutter-stripe/flutter_stripe

Length of output: 647


Add explicitToJson: true to _FlatConfig annotation and regenerate.

The _FlatConfig class is missing the explicitToJson: true parameter in its @JsonSerializable() annotation. As a result, the generated _$FlatConfigToJson() function serializes nested objects (separatorInsets, radio, checkmark, disclosure) as raw objects without calling .toJson(), unlike the properly configured _RowConfig and _EmbeddedPaymentElementAppearance classes. This can cause non-encodable platform-channel payloads.

Suggested source-level fix
-@JsonSerializable()
+@JsonSerializable(explicitToJson: true)
 class _FlatConfig implements FlatConfig {

Regenerate payment_sheet.g.dart after applying this change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart`
around lines 8785 - 8788, The _FlatConfig class annotation is missing
explicitToJson: true which causes nested fields (separatorInsets, radio,
checkmark, disclosure) to be serialized as raw maps instead of calling their
.toJson() methods; update the `@JsonSerializable`() on the _FlatConfig declaration
to `@JsonSerializable`(explicitToJson: true) and then regenerate the generated
part file (payment_sheet.g.dart) so the generated _$FlatConfigToJson() calls
.toJson() for those nested properties.

Comment on lines +120 to +124
if (widget.intentConfiguration.confirmHandler != null) {
Stripe.instance.setConfirmHandler(
widget.intentConfiguration.confirmHandler!,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 134


🏁 Script executed:

cat -n packages/stripe/lib/src/widgets/embedded_payment_element.dart | head -300

Repository: flutter-stripe/flutter_stripe

Length of output: 12124


🏁 Script executed:

# Get the specific line ranges mentioned in the review
sed -n '115,130p' packages/stripe/lib/src/widgets/embedded_payment_element.dart
sed -n '131,150p' packages/stripe/lib/src/widgets/embedded_payment_element.dart
sed -n '270,280p' packages/stripe/lib/src/widgets/embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 1448


🏁 Script executed:

fd -e dart | xargs grep -l "EmbeddedPaymentElement" | head -10

Repository: flutter-stripe/flutter_stripe

Length of output: 511


🏁 Script executed:

# Look for the platform view implementations
rg "_AndroidEmbeddedPaymentElement|_UiKitEmbeddedPaymentElement" -A 15

Repository: flutter-stripe/flutter_stripe

Length of output: 5557


🏁 Script executed:

# Check if there's any mechanism to update native config
rg "creationParams" packages/stripe/lib/src/widgets/embedded_payment_element.dart -B 5 -A 5

Repository: flutter-stripe/flutter_stripe

Length of output: 4107


🏁 Script executed:

# Look for any method to update native configuration after creation
rg "methodChannel|invokeMethod" packages/stripe/lib/src/widgets/embedded_payment_element.dart -B 2 -A 2

Repository: flutter-stripe/flutter_stripe

Length of output: 840


🏁 Script executed:

# Check if there's any platform method for updating config
rg "updateConfiguration|reconfigure|updateIntentConfiguration" packages/stripe/

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

# Look at the full platform view implementations to see if they handle config updates
sed -n '310,400p' packages/stripe/lib/src/widgets/embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 3512


🏁 Script executed:

# Check if there's any comment or documentation about not updating config on rebuild
rg -A 5 -B 5 "didUpdateWidget|configuration" packages/stripe/lib/src/widgets/embedded_payment_element.dart | head -50

Repository: flutter-stripe/flutter_stripe

Length of output: 1536


🏁 Script executed:

# Look for any documentation about how config updates should work
rg -A 10 "EmbeddedPaymentElement" packages/stripe/lib/src/widgets/embedded_payment_element.dart | grep -A 10 "class EmbeddedPaymentElement"

Repository: flutter-stripe/flutter_stripe

Length of output: 789


🏁 Script executed:

# Check the controller to see if it has any config update mechanism
cat -n packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart | head -100

Repository: flutter-stripe/flutter_stripe

Length of output: 2011


🏁 Script executed:

# Check how Stripe.instance.setConfirmHandler works and if it's truly global
rg "setConfirmHandler" packages/stripe -B 3 -A 3

Repository: flutter-stripe/flutter_stripe

Length of output: 1951


🏁 Script executed:

# Verify that platform views only use creationParams once at construction
# by checking Flutter's documentation reference and common patterns
rg "creationParams" packages/stripe/lib/src/widgets/ -B 1 -A 1 | head -40

Repository: flutter-stripe/flutter_stripe

Length of output: 3302


🏁 Script executed:

# Check Stripe class for setConfirmHandler implementation to understand if it's truly global
cat -n packages/stripe/lib/src/stripe.dart | grep -A 10 "setConfirmHandler"

Repository: flutter-stripe/flutter_stripe

Length of output: 612


🏁 Script executed:

# Look at the platform interface to understand if there are any update mechanisms
rg "setConfirmHandler|updateConfiguration" packages/stripe_platform_interface/

Repository: flutter-stripe/flutter_stripe

Length of output: 319


🏁 Script executed:

# Check if there's any way to update the native view after creation
# Look at similar widgets to see patterns
rg "didUpdateWidget" packages/stripe/lib/src/widgets/ -A 10 | head -50

Repository: flutter-stripe/flutter_stripe

Length of output: 4338


🏁 Script executed:

# Verify that creationParams in Flutter platform views are only used at init
# by checking if there's any mechanism to send params after creation
rg "invokeMethod" packages/stripe/lib/src/widgets/embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 200


🏁 Script executed:

# Look at dispose behavior more carefully - check what confirmHandler the widget has
# to understand if dispose clears blindly or conditionally
sed -n '128,135p' packages/stripe/lib/src/widgets/embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 295


🏁 Script executed:

# Check if there are any tests that show expected behavior for config updates
fd -e dart -path "*/test/*" | xargs grep -l "EmbeddedPaymentElement" | head -5

Repository: flutter-stripe/flutter_stripe

Length of output: 241


🏁 Script executed:

# Verify that other similar widgets DO update configs in didUpdateWidget
sed -n '1,100p' packages/stripe/lib/src/widgets/card_field.dart | grep -A 20 "didUpdateWidget"

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

# Find test files properly
find . -name "*test*.dart" -type f | grep -i embedded | head -5

Repository: flutter-stripe/flutter_stripe

Length of output: 55


🏁 Script executed:

# Look at the full card_field didUpdateWidget to see how other widgets handle updates
rg "didUpdateWidget" packages/stripe/lib/src/widgets/card_field.dart -A 20

Repository: flutter-stripe/flutter_stripe

Length of output: 1560


🏁 Script executed:

# Check if EmbeddedPaymentElement's didUpdateWidget should be updating config
# by looking at what parameters can change
sed -n '56,98p' packages/stripe/lib/src/widgets/embedded_payment_element.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 1568


Config changes are not propagated to the native element after initial creation.

didUpdateWidget() only handles controller changes, leaving the native element stale when intentConfiguration, configuration, or confirmHandler changes. While creationParams are built with new values on each rebuild, Flutter platform views only apply them during initial construction—subsequent rebuilds never update the live native instance. Compare with CardField, which actively propagates config changes via invokeMethod() in didUpdateWidget().

Additionally, dispose() unconditionally clears the global confirm handler even if another live element instance is using it, since setConfirmHandler(null) operates on shared global state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart` around lines
120 - 124, didUpdateWidget currently only updates the controller and leaves
native config stale because creationParams aren’t applied after initial platform
view creation; update didUpdateWidget in EmbeddedPaymentElement to detect
changes to intentConfiguration, configuration, and confirmHandler and propagate
them to the native element via the platform channel (invokeMethod) similar to
CardField, sending the new creationParams or specific keys to update the live
native instance; also change dispose to only clear the global confirm handler
(Stripe.instance.setConfirmHandler(null)) if the handler being removed is the
same one that this instance originally set (track the currently set handler on
the instance) to avoid wiping a handler used by another live element.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt (2)

134-183: ⚠️ Potential issue | 🟠 Major

Guarantee JS-awake cleanup and bound JS waits.

keepJsAwakeTask.stop() and embeddedIntentCreationCallback reset still only happen after successful awaits. Move cleanup/reset into finally blocks and wrap JS deferred awaits with a timeout so a lost JS response cannot leak the awake task or suspend indefinitely.

Also applies to: 217-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 134 - 183, The keep-awake task and embeddedIntentCreationCallback
reset are only performed after successful awaits which can leak the awake task
or suspend forever; update the coroutine in EmbeddedPaymentElementView (the
coroutineScope.launch block around KeepJsAwakeTask and
stripeSdkModule.customPaymentMethodResultCallback) to ensure
keepJsAwakeTask.stop() and resetting embeddedIntentCreationCallback happen in a
finally block (so cleanup always runs) and wrap the suspend call to
stripeSdkModule.customPaymentMethodResultCallback.await() with a timeout (e.g.,
using withTimeout or withTimeoutOrNull) so the coroutine unblocks if JS never
responds; also apply the same pattern to the other occurrence around lines
217-221 to guarantee cleanup and bounded waits.

114-132: ⚠️ Potential issue | 🟠 Major

Return a failed CPM result when setup cannot start.

If starting CustomPaymentMethodActivity fails, or requireStripeSdkModule() fails, the callback returns without calling CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...), leaving Stripe waiting for a terminal result.

Suggested direction
           } catch (e: Exception) {
             Log.e("StripeReactNative", "Failed to start CustomPaymentMethodActivity", e)
+            CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(
+              reactContext,
+              CustomPaymentMethodResult.failed(displayMessage = "Unable to start custom payment method confirmation"),
+            )
+            return@remember
           }
@@
             } catch (ex: IllegalArgumentException) {
               Log.e("StripeReactNative", "StripeSdkModule not found for CPM callback", ex)
+              CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(
+                reactContext,
+                CustomPaymentMethodResult.failed(displayMessage = "Unable to confirm custom payment method"),
+              )
               CustomPaymentMethodActivity.finishCurrent()
               return@remember
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 114 - 132, When CustomPaymentMethodActivity cannot be started or
requireStripeSdkModule() throws, ensure you return a terminal failed CPM result
instead of early returning: catch the Exception in the startActivity block and
call CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...) with
a failure/terminal result (indicating setup failure) before logging; similarly,
in the catch for requireStripeSdkModule() (where you log and call
CustomPaymentMethodActivity.finishCurrent()), invoke
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...) with a
failed result prior to returning from the remember block so Stripe receives a
terminal CPM response.
packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt (2)

137-167: ⚠️ Potential issue | 🟠 Major

Thread returnURL into the Android embedded configuration.

The builder still ignores caller-supplied returnURL, while the iOS embedded path validates it. This can break redirect-based payment methods and creates cross-platform behavior drift.

Verify the Stripe Android builder setter name and the platform parity points before patching:

#!/bin/bash
# Description: Confirm Android omission and compare existing returnURL handling.
rg -n -C4 'returnURL|returnUrl|return_url|EmbeddedPaymentElement\\.Configuration|Configuration\\.Builder' \
  --type kt --type swift --type dart
Expected patch shape
     val primaryButtonLabel = readableMap?.getString("primaryButtonLabel")
     val paymentMethodOrder = readableMap?.getStringList("paymentMethodOrder")
+    val returnURL = readableMap?.getString("returnURL")
@@
     primaryButtonLabel?.let { configurationBuilder.primaryButtonLabel(it) }
     paymentMethodOrder?.let { configurationBuilder.paymentMethodOrder(it) }
+    returnURL
+      ?.takeIf { it.isNotBlank() }
+      ?.let { configurationBuilder.returnUrl(it) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`
around lines 137 - 167, The builder never applies caller-supplied returnURL to
EmbeddedPaymentElement.Configuration.Builder, so extract the return URL from
readableMap (e.g., val returnUrl = readableMap?.getString("returnURL") or
"returnUrl") and, after verifying the exact Android setter name on
EmbeddedPaymentElement.Configuration.Builder (returnUrl, returnURL, or similar),
call that setter on configurationBuilder (e.g.,
configurationBuilder.returnUrl(returnUrl)) guarded by a null check; place this
call alongside the other optional setters (near
primaryButtonLabel/paymentMethodOrder) so Android mirrors iOS behavior.

97-115: ⚠️ Potential issue | 🔴 Critical

Don’t convert recoverable config errors into bare Errors.

These catches discard the original PaymentSheetException / PaymentSheetAppearanceException and can crash the prop setter instead of reporting onLoadingFailed with useful details. Preserve the cause and route the failure through the loading-error path.

Suggested direction
       try {
         buildCustomerConfiguration(readableMap)
       } catch (error: PaymentSheetException) {
-        throw Error()
+        throw IllegalArgumentException("Invalid customer configuration", error)
       }
@@
       try {
         buildPaymentSheetAppearance(readableMap?.getMap("appearance"), context)
       } catch (error: PaymentSheetAppearanceException) {
-        throw Error()
+        throw IllegalArgumentException("Invalid payment sheet appearance", error)
       }

Prefer catching these in setConfiguration(...) and invoking view.onLoadingFailed(...) rather than rethrowing from the React prop setter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`
around lines 97 - 115, In EmbeddedPaymentElementViewManager (e.g., in the prop
handling where buildCustomerConfiguration and buildPaymentSheetAppearance are
called), stop converting PaymentSheetException/PaymentSheetAppearanceException
into bare Error() throws; instead catch those exceptions and route them to the
view's loading-error path—call view.onLoadingFailed(error) (or a similar failure
callback) and return/assign a safe fallback (e.g., null) so setConfiguration or
the host can handle the failure without crashing; do this in the
setConfiguration/property setter surrounding buildCustomerConfiguration and
buildPaymentSheetAppearance so the original exception is preserved and
propagated via onLoadingFailed rather than rethrown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`:
- Around line 265-274: The current logic in EmbeddedPaymentElementView sets an
immediate-action rowSelectionBehavior for any non-Default
RowSelectionBehaviorType (including null), which flips the UX; update the branch
that builds rowSelectionBehavior to only create
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction when
rowSelectionBehaviorType is explicitly ImmediateAction (and otherwise use
EmbeddedPaymentElement.RowSelectionBehavior.default()), i.e., check for the
ImmediateAction enum/value rather than treating all non-Default as
immediateAction, and still invoke onRowSelectionImmediateAction?.invoke() ?:
requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
inside that immediateAction branch.
- Line 99: Replace the hard-cast property and module lookup by injecting
dependencies into the view: remove the reactContext getter that casts to
ThemedReactContext and stop calling requireStripeSdkModule(); instead add
constructor parameters (e.g., stripeSdkModule: StripeSdkModule, onEvent:
(Event)->Unit or specific callback interfaces) to EmbeddedPaymentElementView and
store them as fields; update any code that called requireStripeSdkModule() (and
any uses of reactContext) to use the injected stripeSdkModule and callbacks;
ensure callers (view factory / host) supply these dependencies when creating
EmbeddedPaymentElementView so the class no longer depends on ThemedReactContext.

---

Duplicate comments:
In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`:
- Around line 134-183: The keep-awake task and embeddedIntentCreationCallback
reset are only performed after successful awaits which can leak the awake task
or suspend forever; update the coroutine in EmbeddedPaymentElementView (the
coroutineScope.launch block around KeepJsAwakeTask and
stripeSdkModule.customPaymentMethodResultCallback) to ensure
keepJsAwakeTask.stop() and resetting embeddedIntentCreationCallback happen in a
finally block (so cleanup always runs) and wrap the suspend call to
stripeSdkModule.customPaymentMethodResultCallback.await() with a timeout (e.g.,
using withTimeout or withTimeoutOrNull) so the coroutine unblocks if JS never
responds; also apply the same pattern to the other occurrence around lines
217-221 to guarantee cleanup and bounded waits.
- Around line 114-132: When CustomPaymentMethodActivity cannot be started or
requireStripeSdkModule() throws, ensure you return a terminal failed CPM result
instead of early returning: catch the Exception in the startActivity block and
call CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...) with
a failure/terminal result (indicating setup failure) before logging; similarly,
in the catch for requireStripeSdkModule() (where you log and call
CustomPaymentMethodActivity.finishCurrent()), invoke
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...) with a
failed result prior to returning from the remember block so Stripe receives a
terminal CPM response.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`:
- Around line 137-167: The builder never applies caller-supplied returnURL to
EmbeddedPaymentElement.Configuration.Builder, so extract the return URL from
readableMap (e.g., val returnUrl = readableMap?.getString("returnURL") or
"returnUrl") and, after verifying the exact Android setter name on
EmbeddedPaymentElement.Configuration.Builder (returnUrl, returnURL, or similar),
call that setter on configurationBuilder (e.g.,
configurationBuilder.returnUrl(returnUrl)) guarded by a null check; place this
call alongside the other optional setters (near
primaryButtonLabel/paymentMethodOrder) so Android mirrors iOS behavior.
- Around line 97-115: In EmbeddedPaymentElementViewManager (e.g., in the prop
handling where buildCustomerConfiguration and buildPaymentSheetAppearance are
called), stop converting PaymentSheetException/PaymentSheetAppearanceException
into bare Error() throws; instead catch those exceptions and route them to the
view's loading-error path—call view.onLoadingFailed(error) (or a similar failure
callback) and return/assign a safe fallback (e.g., null) so setConfiguration or
the host can handle the failure without crashing; do this in the
setConfiguration/property setter surrounding buildCustomerConfiguration and
buildPaymentSheetAppearance so the original exception is preserved and
propagated via onLoadingFailed rather than rethrown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0de921b0-786e-4d41-aeb9-80b0ccac5aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf238e and 14b785f.

📒 Files selected for processing (4)
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentElementConfig.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt

var onRowSelectionImmediateAction: (() -> Unit)? = null
var onFormSheetConfirmComplete: ((Map<String, Any>) -> Unit)? = null

private val reactContext get() = context as ThemedReactContext

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect all EmbeddedPaymentElementView construction and RN-module dependencies.
fd -i 'StripeSdkEmbeddedPaymentElementPlatformView.kt|EmbeddedPaymentElementView.kt|EmbeddedPaymentElementViewManager.kt' -t f -x sed -n '1,260p' {}
rg -n -C4 'EmbeddedPaymentElementView\\(|ThemedReactContext|requireStripeSdkModule\\(|embeddedIntentCreationCallback|emitOnConfirmHandlerCallback'

Repository: flutter-stripe/flutter_stripe

Length of output: 22456


🏁 Script executed:

# Find the total lines in the file and examine line 412 and surrounding context
wc -l packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 179


🏁 Script executed:

# Get line 412 and context around it
sed -n '400,425p' packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt

Repository: flutter-stripe/flutter_stripe

Length of output: 800


🏁 Script executed:

# Search for the requireStripeSdkModule function definition
rg -n 'fun requireStripeSdkModule|private fun requireStripeSdkModule' packages/stripe_android

Repository: flutter-stripe/flutter_stripe

Length of output: 294


🏁 Script executed:

# Search for all instantiations of EmbeddedPaymentElementView to see all code paths
rg -n 'EmbeddedPaymentElementView\(' packages/stripe_android --type kotlin -A 2 -B 2

Repository: flutter-stripe/flutter_stripe

Length of output: 1501


Remove the hard cast and inject context dependencies instead of assuming ThemedReactContext.

The view hard-casts context to ThemedReactContext (line 99) and requireStripeSdkModule() (line 412) depends on this assumption. While Flutter does instantiate the view with a ThemedReactContext, hard-casting without guards is fragile. Instead, pass StripeSdkModule and event callbacks directly to the view constructor—this eliminates the RN-specific type dependency and makes the view reusable across platforms without relying on reflection or module casting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
at line 99, Replace the hard-cast property and module lookup by injecting
dependencies into the view: remove the reactContext getter that casts to
ThemedReactContext and stop calling requireStripeSdkModule(); instead add
constructor parameters (e.g., stripeSdkModule: StripeSdkModule, onEvent:
(Event)->Unit or specific callback interfaces) to EmbeddedPaymentElementView and
store them as fields; update any code that called requireStripeSdkModule() (and
any uses of reactContext) to use the injected stripeSdkModule and callbacks;
ensure callers (view factory / host) supply these dependencies when creating
EmbeddedPaymentElementView so the class no longer depends on ThemedReactContext.

Comment on lines +265 to +274
.rowSelectionBehavior(
if (type == RowSelectionBehaviorType.Default) {
EmbeddedPaymentElement.RowSelectionBehavior.default()
} else {
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
onRowSelectionImmediateAction?.invoke() ?: run {
requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
}
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Default to normal row selection unless ImmediateAction is explicitly requested.

rowSelectionBehaviorType starts as null, so the current else branch creates immediate-action behavior before props settle. That can flip the default UX for callers that never requested it.

Proposed fix
           .rowSelectionBehavior(
-            if (type == RowSelectionBehaviorType.Default) {
-              EmbeddedPaymentElement.RowSelectionBehavior.default()
-            } else {
+            if (type == RowSelectionBehaviorType.ImmediateAction) {
               EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
                 onRowSelectionImmediateAction?.invoke() ?: run {
                   requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
                 }
               }
+            } else {
+              EmbeddedPaymentElement.RowSelectionBehavior.default()
             },
           )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.rowSelectionBehavior(
if (type == RowSelectionBehaviorType.Default) {
EmbeddedPaymentElement.RowSelectionBehavior.default()
} else {
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
onRowSelectionImmediateAction?.invoke() ?: run {
requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
}
}
},
.rowSelectionBehavior(
if (type == RowSelectionBehaviorType.ImmediateAction) {
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
onRowSelectionImmediateAction?.invoke() ?: run {
requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
}
}
} else {
EmbeddedPaymentElement.RowSelectionBehavior.default()
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 265 - 274, The current logic in EmbeddedPaymentElementView sets an
immediate-action rowSelectionBehavior for any non-Default
RowSelectionBehaviorType (including null), which flips the UX; update the branch
that builds rowSelectionBehavior to only create
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction when
rowSelectionBehaviorType is explicitly ImmediateAction (and otherwise use
EmbeddedPaymentElement.RowSelectionBehavior.default()), i.e., check for the
ImmediateAction enum/value rather than treating all non-Default as
immediateAction, and still invoke onRowSelectionImmediateAction?.invoke() ?:
requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
inside that immediateAction branch.

@remonh87

Copy link
Copy Markdown
Member

I think you need to rabase this on the latest main branch but then we can integrate it 👍

…irm()

Changes confirm() method to return payment result instead of void,
allowing Flutter apps to detect payment failures without relying solely
on callbacks.

- Modified EmbeddedPaymentElementController.confirm() return type
- Updated embedded_payment_element.dart to capture method channel result
- iOS implementation complete with result dictionary
- Android implementation incomplete (help wanted)

Fixes detection of payment failures (insufficient funds, card declined, etc.)
Follows React Native SDK pattern where confirm() returns result status.
- Added onConfirmResult callback property to EmbeddedPaymentElementView
- Modified resultCallback to invoke Flutter method channel callback
- Updated platform view to set callback before calling confirm()
- Callback returns Map with status and optional error message
- Matches iOS implementation for consistent cross-platform behavior
…errors

- Removed updateConfiguration and updateIntentConfiguration from method channel handler
- These methods were unused and causing Dynamic type mismatch compile errors
- Configuration is set once during initialization and doesn't need updates
- Added Jetpack Compose BOM and core dependencies (ui, foundation, runtime)
- Updated Kotlin from 1.8.0 to 1.9.0 for Compose compatibility
- Added Compose build features and compiler extension version
- Fixes 'Unresolved reference' errors for Box, requiredHeight, foundation
- Required for EmbeddedPaymentElement which uses Compose UI
- Cast entry.value to Dynamic for ViewManager methods
- Change array.size() to array.size (property not function)
- Fixes Kotlin 2.0 compilation errors
Android would crash when intentConfiguration was provided. Fixed by updating how payment data is passed to native code.
Fixed compilation errors in Bundle handling and upgraded Gradle to 8.11.1 to match React Native SDK.
Height changes were sent to wrong channel. Widget now receives events
directly from platform view instead of global emitter.

Added callbacks for all events:
- onHeightChanged
- onPaymentOptionChanged
- onLoadingFailed
- onRowSelectionImmediateAction
- onFormSheetConfirmComplete

Matches iOS behavior.
@programmeraditya

Copy link
Copy Markdown
Contributor Author

@remonh87 it's ready to merge now. could you confirm? Thank you!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (11)
packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt (2)

29-47: ⚠️ Potential issue | 🟠 Major

channel.invokeMethod must run on the main thread.

These callbacks are driven from EmbeddedPaymentElementView's Compose effects and onSizeChanged. While those typically run on main, reportHeightChange is reached via onSizeChanged (main) but embedded.paymentOption.collect { ... onPaymentOptionChanged?.invoke(...) } runs on whichever dispatcher the coroutine scope was created in. Dispatching each channel.invokeMethod through Handler(Looper.getMainLooper()).post { ... } (or verify-then-document the dispatcher invariant) would eliminate a latent threading violation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`
around lines 29 - 47, The channel.invokeMethod calls in
StripeSdkEmbeddedPaymentElementPlatformView's embeddedView callbacks
(onHeightChanged, onPaymentOptionChanged, onLoadingFailed,
onRowSelectionImmediateAction, onFormSheetConfirmComplete) must be executed on
the Android main thread; update each callback to post the invokeMethod
invocation to the main looper (e.g., use Handler(Looper.getMainLooper()).post {
... } or an equivalent runOnUiThread) so that channel.invokeMethod(...) is
always called from the main thread to avoid threading violations.

92-98: ⚠️ Potential issue | 🟠 Major

confirm path still leaks onConfirmResult / hangs Flutter on a synchronous throw.

If viewManager.confirm(embeddedView) throws synchronously (e.g., the view manager's confirm() raises before trySend queues the event), the onConfirmResult callback is never invoked and Flutter's confirm() future hangs forever. Wrap in try/catch, clear the callback, and surface via result.error(...).

🔧 Proposed fix
             "confirm" -> {
                 embeddedView.onConfirmResult = { resultMap ->
                     result.success(resultMap)
                     embeddedView.onConfirmResult = null
                 }
-                viewManager.confirm(embeddedView)
+                try {
+                    viewManager.confirm(embeddedView)
+                } catch (e: Exception) {
+                    embeddedView.onConfirmResult = null
+                    result.error("confirm_failed", e.message, null)
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`
around lines 92 - 98, The confirm branch sets embeddedView.onConfirmResult but
does not handle synchronous exceptions from viewManager.confirm, which can leak
the callback and hang Flutter; wrap the call to
viewManager.confirm(embeddedView) in try/catch/finally: set
embeddedView.onConfirmResult before the call, in catch call result.error(...)
with the exception details, and in finally clear embeddedView.onConfirmResult =
null to ensure it is never left set; use the same symbols
(embeddedView.onConfirmResult, viewManager.confirm, result.success/result.error)
so the fix is applied to StripeSdkEmbeddedPaymentElementPlatformView.
packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt (4)

265-275: ⚠️ Potential issue | 🟠 Major

rowSelectionBehaviorType default (null) currently maps to ImmediateAction.

rowSelectionBehaviorType is initialized to null (L90). During the first composition before props settle, type != RowSelectionBehaviorType.Default (because it's null), so the builder installs immediateAction {...} — flipping the default UX for callers that never asked for it. Match the intended branch explicitly.

🔧 Proposed fix
           .rowSelectionBehavior(
-            if (type == RowSelectionBehaviorType.Default) {
-              EmbeddedPaymentElement.RowSelectionBehavior.default()
-            } else {
-              EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
-                onRowSelectionImmediateAction?.invoke() ?: run {
-                  requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
-                }
-              }
-            },
+            if (type == RowSelectionBehaviorType.ImmediateAction) {
+              EmbeddedPaymentElement.RowSelectionBehavior.immediateAction {
+                onRowSelectionImmediateAction?.invoke() ?: run {
+                  requireStripeSdkModule().eventEmitter.emitEmbeddedPaymentElementRowSelectionImmediateAction()
+                }
+              }
+            } else {
+              EmbeddedPaymentElement.RowSelectionBehavior.default()
+            },
           )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 265 - 275, The current branch for .rowSelectionBehavior treats a
null initial prop as non-default and installs ImmediateAction; update the
conditional to explicitly check for the Default enum value (and treat null as
Default) so only when type == RowSelectionBehaviorType.Default call
EmbeddedPaymentElement.RowSelectionBehavior.default(), otherwise call
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction { ... }. Locate the
code building rowSelectionBehavior (the variable named type and the call to
.rowSelectionBehavior, and the enum RowSelectionBehaviorType) and change the
condition to explicitly compare type to RowSelectionBehaviorType.Default (or
treat null via a safe coalescing/defaulting expression) before choosing
immediateAction.

99-99: ⚠️ Potential issue | 🟡 Minor

Hard-cast to ThemedReactContext and module lookup couples the view to RN.

context as ThemedReactContext will crash if this view is ever constructed from Flutter with a non-RN context (the Flutter factory side already builds a ThemedReactContext to satisfy this coupling — see StripeSdkEmbeddedPaymentElementPlatformView.kt L24). Injecting StripeSdkModule and the event callbacks as constructor parameters would remove the hidden requirement and make multi-host usage (RN + Flutter) safer.

Also applies to: 412-412

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
at line 99, The view currently hard-casts context in the reactContext property
(reactContext get() = context as ThemedReactContext) which ties
EmbeddedPaymentElementView to React Native and can crash under other hosts;
change the constructor of EmbeddedPaymentElementView to accept a
ThemedReactContext (or a generic Context) plus injected dependencies: the
StripeSdkModule instance and any event callback lambdas, remove the hard-cast
reactContext property and use the injected references instead, and update all
usages (including the other occurrence where reactContext is referenced) to use
the injected module/callbacks so the view no longer assumes RN-specific context.

191-232: ⚠️ Potential issue | 🟠 Major

createIntentCallback leaks state on failure and silently serializes multi-instance flows.

  1. Cleanup ordering: keepJsAwakeTask.stop() (L221) and embeddedIntentCreationCallback = CompletableDeferred() (L219) run only on the happy path. If the await throws / is cancelled, the keep-awake lock leaks and the next confirm() will pick up the stale deferred. Wrap both lines in try { … } finally { … }.
  2. embeddedIntentCreationCallback and customPaymentMethodResultCallback live on the StripeSdkModule singleton, so two concurrent EmbeddedPaymentElement instances (or overlapping confirm retries) share the same CompletableDeferred — the first to resolve it wins and the other call suspends indefinitely. Worth documenting this as a known limitation for multi-instance use, or routing these callbacks per-view.
  3. Consider withTimeoutOrNull(...) on await() so a lost JS response surfaces as a CreateIntentResult.Failure rather than a hang.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 191 - 232, The createIntentCallback leaks state and can deadlock
across instances: wrap the await and subsequent cleanup in a try/finally so
keepJsAwakeTask.stop() and stripeSdkModule.embeddedIntentCreationCallback =
CompletableDeferred() always run even if await() throws or is cancelled; replace
the direct await() call with a timed wait (e.g., withTimeoutOrNull) so a missing
JS response returns a CreateIntentResult.Failure instead of hanging; and
eliminate the singleton-per-process collision by routing callbacks per-view
(e.g., include a unique requestId when emitting event and store
CompletableDeferred objects in a map keyed by that id, or document the
single-instance limitation) — update references: createIntentCallback,
KeepJsAwakeTask, stripeSdkModule.embeddedIntentCreationCallback,
stripeSdkModule.eventEmitter.emitOnConfirmHandlerCallback, and
embeddedIntentCreationCallback.await().

113-183: ⚠️ Potential issue | 🟠 Major

Missing CustomPaymentMethodResult.failed(...) on every early-exit path — Stripe SDK hangs.

Three related paths still don't notify Stripe:

  • L114–123: startActivity failure only logs and falls through; the coroutineScope.launch below then awaits customPaymentMethodResultCallback, which no JS listener will ever resolve → the payment flow hangs forever.
  • L125–132: requireStripeSdkModule() failure returns from the outer lambda without calling CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(...) — Stripe keeps waiting on our callback.
  • L138–183: If customPaymentMethodResultCallback.await() throws (e.g., cancellation), keepJsAwakeTask.stop() on L152 is skipped; the only finally is for finishCurrent().

Put keepJsAwakeTask.stop() in finally, and on every error path call CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(reactContext, CustomPaymentMethodResult.failed(...)) before returning. Also consider withTimeoutOrNull(...) around the await to bound hangs when JS never answers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`
around lines 113 - 183, The startActivity catch, the requireStripeSdkModule()
catch, and the coroutine await failure paths never return a failed
CustomPaymentMethodResult to Stripe, causing hangs; update the startActivity
exception handler and the requireStripeSdkModule() catch to call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(reactContext,
CustomPaymentMethodResult.failed(displayMessage = "...")) before returning, and
inside the coroutineScope.launch wrap the await call
(stripeSdkModule.customPaymentMethodResultCallback.await()) with a bounded
timeout (e.g., withTimeoutOrNull) and ensure KeepJsAwakeTask.stop() is invoked
in a finally block so it always stops, and on any null/exception path convert to
CustomPaymentMethodResult.failed(...) and pass it to
CustomPaymentMethodResultHandler before finishing and calling
CustomPaymentMethodActivity.finishCurrent().
packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt (2)

137-167: ⚠️ Potential issue | 🟠 Major

returnURL is silently dropped on Android — cross-platform parity gap.

iOS validates returnURL as required (StripeSdkImpl+Embedded.swift Lines 64–68), but this builder never reads readableMap?.getString("returnURL"), so any caller-supplied value is discarded on Android. At minimum, pass it through to EmbeddedPaymentElement.Configuration.Builder so apps relying on Link/redirect flows behave the same on both platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`
around lines 137 - 167, The builder chain for
EmbeddedPaymentElement.Configuration in EmbeddedPaymentElementViewManager.kt is
missing the returnURL from the incoming readableMap, causing returnURL to be
dropped on Android; update the configuration building to read
readableMap?.getString("returnURL") and call the corresponding Builder method
(e.g., .returnURL(...)) before .build() so the value is passed through (locate
the EmbeddedPaymentElement.Configuration.Builder(...) block and add the
.returnURL(readableMap?.getString("returnURL")) invocation, preserving
null-safety consistent with other fields like linkConfig and googlePayConfig).

97-115: ⚠️ Potential issue | 🔴 Critical

Still rethrowing recoverable parse failures as Error — drops the original cause.

throw Error() creates a bare kotlin.Error / java.lang.Error (the superclass of OutOfMemoryError etc.), which:

  1. Loses the caught exception entirely (no cause, no message).
  2. Is semantically reserved for unrecoverable VM errors — most frameworks (including RN's error boundary) won't catch it, so it propagates out of setConfiguration and can crash the host.

Surface failures through view.onLoadingFailed(...) / event emission with the original PaymentSheetException / PaymentSheetAppearanceException as cause, the same way PaymentSheetManager does.

🔧 Suggested direction
     val customerConfiguration =
       try {
         buildCustomerConfiguration(readableMap)
       } catch (error: PaymentSheetException) {
-        throw Error()
+        view.onLoadingFailed?.invoke(
+          EmbeddedPaymentElementLoadingError(
+            message = error.message ?: "Invalid customer configuration",
+            code = "invalid_customer_configuration",
+            details = null,
+          ),
+        )
+        return@setConfiguration // caller path needs a matching early-return
       }

(You'll also need to propagate that early-return through setConfiguration or guard subsequent access.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`
around lines 97 - 115, The try/catch blocks in EmbeddedPaymentElementViewManager
(around buildCustomerConfiguration and buildPaymentSheetAppearance) currently
throw a bare Error, losing the original exception and risking an uncaught VM
error; instead catch PaymentSheetException and PaymentSheetAppearanceException
and surface the original exception via view.onLoadingFailed(...) or an RN event
(include the caught exception as the cause/message), then abort or return from
setConfiguration (or guard later usage of customerConfiguration/appearance) so
subsequent code doesn’t run after a parse failure—mirror the behavior used in
PaymentSheetManager for error propagation.
packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift (2)

190-192: ⚠️ Potential issue | 🟠 Major

window?.rootViewController misses modals/navigation stacks.

Assigning window?.rootViewController will present authentication / form sheets from the wrong controller when a modal is active. The codebase already has findViewControllerPresenter() in StripeSdkImpl.swift that traverses presentedViewController — please use it in both attachEmbeddedView and embeddedPaymentElementWillPresent.

Also applies to: 248-251

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`
around lines 190 - 192, Replace direct use of window?.rootViewController when
setting embeddedElement.presentingViewController with the app's view-controller
traversal helper to avoid presenting behind modals; specifically, in
attachEmbeddedView and embeddedPaymentElementWillPresent locate the presenting
controller via StripeSdkImpl.findViewControllerPresenter() (or the equivalent
function that walks presentedViewController) and assign that controller to
embeddedElement.presentingViewController instead of using
embeddedView.window?.rootViewController so presentation honors active
modal/navigation stacks.

120-163: ⚠️ Potential issue | 🟠 Major

Shared-singleton routing still present; confirm/clear will misroute with multiple views.

StripeSdkImpl.shared.createEmbeddedPaymentElement(...) stores the created element on self.embeddedInstance (see StripeSdkImpl+Embedded.swift line 78). Creating a second EmbeddedPaymentElement widget while the first is still on screen will overwrite embeddedInstance, and since handle(...) at Lines 200–213 routes confirm/clearPaymentOption through the same shared impl, actions from the first widget's Flutter channel will silently operate on the second widget's element (and its delegate/attachedView). Please either reject creating a second instance explicitly, or refactor the iOS side to key embedded instances per viewId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`
around lines 120 - 163, The code stores the created element in a shared
singleton (StripeSdkImpl.shared.embeddedInstance) which allows a second
EmbeddedPaymentElement to overwrite the first and misroute confirm/clear calls;
change createEmbeddedPaymentElement usage in EmbeddedPaymentElementFactory.swift
so it either (A) rejects creation when StripeSdkImpl.shared.embeddedInstance is
already non-nil/attached by returning an error via the
reject/embeddedPaymentElementLoadingFailed callback (use the same error payload
shape), or (B) refactors the iOS side to track instances per viewId (e.g., keep
a dictionary keyed by viewId inside StripeSdkImpl and update
createEmbeddedPaymentElement, embeddedInstance references, and
handle(confirm/clearPaymentOption) to use the viewId key) so
confirm/clearPaymentOption/handle(...) route to the correct instance instead of
the single shared embeddedInstance.
packages/stripe/lib/src/widgets/embedded_payment_element.dart (1)

120-144: ⚠️ Potential issue | 🟠 Major

Config changes aren't propagated after initial creation, and dispose() wipes a global handler unconditionally.

Two pre-existing concerns still apply after this round:

  1. didUpdateWidget only handles controller changes — if the caller rebuilds with a new intentConfiguration, configuration, or confirmHandler, the live native instance stays on the stale config (native only reads creationParams once). Consider mirroring the card_field.dart pattern and invoking a method-channel update, or at least calling setConfirmHandler again when widget.intentConfiguration.confirmHandler changes.
  2. dispose() calls Stripe.instance.setConfirmHandler(null) whenever the outgoing widget had a handler. Since setConfirmHandler operates on shared global state, unmounting one EmbeddedPaymentElement can blow away another live instance's handler. Track whether this state installed the handler and only clear it when that's still the active one.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart` around lines
120 - 144, Update didUpdateWidget and dispose to correctly propagate config
changes and avoid clobbering global state: in didUpdateWidget (in addition to
controller changes) detect changes to widget.intentConfiguration,
widget.configuration, and widget.intentConfiguration.confirmHandler and call the
native update path (mirror card_field.dart — e.g., invoke the existing
method-channel update or re-send creationParams) and re-run
Stripe.instance.setConfirmHandler when the confirmHandler reference changes; in
dispose, stop unconditionally clearing the global handler by tracking ownership
(add a private field like _installedConfirmHandler or _confirmHandlerOwner set
when you call Stripe.instance.setConfirmHandler(thisHandler)) and only call
Stripe.instance.setConfirmHandler(null) if this widget installed the active
handler, leaving other widgets’ handlers untouched.
🧹 Nitpick comments (7)
packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt (1)

13-13: Redundant import — already covered by wildcard.

Line 10 already wildcard-imports com.reactnativestripesdk.*, so this explicit import is dead noise and can be removed.

♻️ Diff
 import com.reactnativestripesdk.*
 import com.reactnativestripesdk.addresssheet.AddressSheetViewManager
 import com.reactnativestripesdk.pushprovisioning.AddToWalletButtonManager
-import com.reactnativestripesdk.EmbeddedPaymentElementViewManager
 import com.reactnativestripesdk.utils.getIntOrNull
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt`
at line 13, Remove the redundant explicit import of
EmbeddedPaymentElementViewManager since com.reactnativestripesdk.* already
covers it; locate the import line importing EmbeddedPaymentElementViewManager in
StripeAndroidPlugin.kt and delete that single import so only the wildcard import
remains (no other code changes needed).
packages/stripe/lib/src/stripe.dart (1)

571-575: Doc nit: mention the clear-handler semantics.

Passing null to this setter unregisters the handler on the native side — worth calling out so consumers of EmbeddedPaymentElement know to clear the callback on dispose to avoid retaining stale closures.

💡 Suggested docstring
-  /// Registers a callback that the native embedded element invokes when it
-  /// needs the app to create an intent client secret.
+  /// Registers a callback that the native embedded element invokes when it
+  /// needs the app to create an intent client secret.
+  ///
+  /// Pass `null` to clear a previously registered handler (e.g. when the
+  /// embedded element widget is disposed).
   void setConfirmHandler(ConfirmHandler? handler) {
     _platform.setConfirmHandler(handler);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/stripe.dart` around lines 571 - 575, The docstring
for setConfirmHandler should mention that passing null unregisters the native
callback so callers can clear it (e.g., on dispose) to avoid retaining stale
closures; update the comment on void setConfirmHandler(ConfirmHandler? handler)
(and mention ConfirmHandler and _platform.setConfirmHandler) to explicitly state
the clear-handler semantics and recommend calling setConfirmHandler(null) when
disposing EmbeddedPaymentElement.
packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+Embedded.swift (1)

58-68: Nitpick: simplify the nil/empty checks.

The == nil || ?.isEmpty == true pattern is a bit verbose — an empty-or-nil check via ?? or isNilOrEmpty helper reads cleaner and avoids the redundant optional ladder.

♻️ Suggested simplification
-    if STPAPIClient.shared.publishableKey == nil || STPAPIClient.shared.publishableKey?.isEmpty == true {
-      let errorMsg = "Stripe publishableKey is not set"
-      resolve(Errors.createError(ErrorType.Failed, errorMsg))
-      return
-    }
-
-    if configuration.returnURL == nil || configuration.returnURL?.isEmpty == true {
-      let errorMsg = "returnURL is required for EmbeddedPaymentElement"
-      resolve(Errors.createError(ErrorType.Failed, errorMsg))
-      return
-    }
+    if (STPAPIClient.shared.publishableKey ?? "").isEmpty {
+      resolve(Errors.createError(ErrorType.Failed, "Stripe publishableKey is not set"))
+      return
+    }
+
+    if (configuration.returnURL ?? "").isEmpty {
+      resolve(Errors.createError(ErrorType.Failed, "returnURL is required for EmbeddedPaymentElement"))
+      return
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe`
Sdk/StripeSdkImpl+Embedded.swift around lines 58 - 68, Replace the verbose
nil-or-empty checks with concise empty-or-nil checks: for
STPAPIClient.shared.publishableKey and configuration.returnURL use a single
condition that treats nil as empty (e.g. coalesce to "" or use a helper like
isNilOrEmpty) so the ifs become simpler and equivalent; update the branches
around STPAPIClient.shared.publishableKey and configuration.returnURL in
StripeSdkImpl+Embedded.swift (the resolve(Errors.createError(...)) calls and
returns remain unchanged).
packages/stripe/lib/src/widgets/embedded_payment_element.dart (2)

302-302: Hard-coded fallback height of 400 can clip content on first render.

Before the first onHeightChanged arrives, the platform view is forced into a 400 logical-pixel box. On screens/themes where the initial natural height exceeds 400 (saved payment methods + link/apple pay), the view will be vertically clipped (ClipRect on iOS) for the brief window until native reports back. Consider using double.infinity with a parent constraint, or at least plumbing a initialHeight parameter through the widget for callers who know the expected size.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart` at line 302,
The hard-coded fallback of 400 in the EmbeddedPaymentElement causes clipping
before native onHeightChanged reports; update the widget to accept an optional
initialHeight parameter (e.g., add final double? initialHeight to the
EmbeddedPaymentElement constructor) and change the height decision from
"_currentHeight > 0 ? _currentHeight : 400" to use the initialHeight or an
unconstrained alternative (e.g., "_currentHeight > 0 ? _currentHeight :
(widget.initialHeight ?? double.infinity)"), or document callers should wrap in
a parent with constraints if double.infinity is used; ensure any platform view
container code that references _currentHeight uses this new fallback so initial
render won't be clipped.

176-224: Blanket catch (e) + debugPrint hides real bugs from callers.

Wrapping all method-call handlers in a single try / catch and only debugPrinting is convenient but swallows parse/serialization bugs (e.g., malformed paymentOption payloads) so onLoadingFailed / onPaymentOptionChanged silently fail. At minimum, surface unexpected errors through widget.onLoadingFailed (or a dedicated diagnostics callback) so callers can actually observe them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart` around lines
176 - 224, The blanket try/catch around the platform method dispatch swallows
real errors; change the catch block in the method that processes `call` (the
handler containing switch on call.method) to forward unexpected exceptions to
the consumer via `widget.onLoadingFailed` instead of only calling debugPrint:
capture the caught exception and convert it into the same error type returned by
`_parseLoadingError` (or construct a simple loading error wrapper) and invoke
`widget.onLoadingFailed?.call(...)`, then still log the error with debugPrint;
leave regular per-case error handling (e.g. parsing for
`onPaymentOptionChanged`, `onHeightChanged`) intact so parse/serialization
failures are surfaced to callers.
packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart (1)

3-53: Controller extends ChangeNotifier but never notifies — release-mode assertions also silently pass.

  1. The controller extends ChangeNotifier but attach/detach/dispose never call notifyListeners(). Anyone who adds a listener (e.g., a UI that wants to react to hasEmbeddedPaymentElement changes) will see nothing. Either call notifyListeners() on attach/detach, or drop the ChangeNotifier base if listener-based reactivity isn't the design intent.
  2. confirm() and clearPaymentOption() protect against "not attached" only via assert(...) which is stripped in release. If detached in release, they silently resolve to null / no-op, which is a tricky-to-debug footgun. Consider throwing a StateError so misuse is loud in production too.
♻️ Suggested refactor
   void attach(EmbeddedPaymentElementContext context) {
     assert(
       !hasEmbeddedPaymentElement,
       'Controller is already attached to an EmbeddedPaymentElement',
     );
     _context = context;
+    notifyListeners();
   }

   void detach(EmbeddedPaymentElementContext context) {
     if (_context == context) {
       _context = null;
+      notifyListeners();
     }
   }

   Future<Map<String, dynamic>?> confirm() async {
-    assert(
-      hasEmbeddedPaymentElement,
-      'Controller must be attached to an EmbeddedPaymentElement',
-    );
-    return await _context?.confirm();
+    final context = _context;
+    if (context == null) {
+      throw StateError('Controller is not attached to an EmbeddedPaymentElement');
+    }
+    return context.confirm();
   }

   Future<void> clearPaymentOption() async {
-    assert(
-      hasEmbeddedPaymentElement,
-      'Controller must be attached to an EmbeddedPaymentElement',
-    );
-    await _context?.clearPaymentOption();
+    final context = _context;
+    if (context == null) {
+      throw StateError('Controller is not attached to an EmbeddedPaymentElement');
+    }
+    await context.clearPaymentOption();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart`
around lines 3 - 53, The controller currently extends ChangeNotifier but never
calls notifyListeners and its methods confirm, clearPaymentOption, and
disposeView only assert attachment (stripped in release), so update
EmbeddedPaymentElementController: call notifyListeners() at the end of
attach(EmbeddedPaymentElementContext), detach(EmbeddedPaymentElementContext) and
override dispose() (notifyListeners before clearing _context) to inform
listeners of state changes; replace the assert guards in confirm(),
clearPaymentOption(), and disposeView() with runtime checks that throw a
StateError with a clear message (e.g., "Controller must be attached to an
EmbeddedPaymentElement") when _context is null so misuse fails loudly in
production; keep using _context?.method() only after verifying non-null.
packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt (1)

55-67: Consider renaming the ReadableMap wrapper to avoid shadowing React Native's interface.

Lines 57 and 67 use ReadableMap(configMap as Map<String, Any>), which compiles because a custom wrapper class named ReadableMap is defined in packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReadableMap.java. However, this wrapper intentionally shadows React Native's ReadableMap interface—both share the identical package name com.facebook.react.bridge. While this works today, it creates confusion and maintainability risk, as developers may mistakenly assume they're working with the RN interface. Rename the wrapper class to a distinct name (e.g., RNReadableMap, ReadableMapWrapper) and update all usages across the codebase to make the shadowing explicit and avoid future errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`
around lines 55 - 67, The custom wrapper class ReadableMap (in package
com.facebook.react.bridge) shadows React Native's ReadableMap interface; rename
the wrapper to a distinct name (e.g., RNReadableMap or ReadableMapWrapper) and
update all usages: replace constructor calls in
StripeSdkEmbeddedPaymentElementPlatformView (where ReadableMap(configMap as
Map<String, Any>) and ReadableMap(intentConfigMap as Map<String, Any>) are
used), and update any other references across the codebase (including the
wrapper class file and imports) so parseRowSelectionBehavior,
parseElementConfiguration, and other call sites use the new wrapper type and
imports to avoid name collision with RN's interface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt`:
- Around line 85-106: The current mapToBundle branch for List handling drops or
mis-types many element shapes; update the List<*> branch in mapToBundle to
inspect element types and convert accordingly: when elements are Map (e.g.,
Map<String, Any>) convert each to a Bundle via the existing mapToBundle
recursion and put an ArrayList<Bundle> into result; handle Long, Double, Boolean
and nullable/mixed-typed lists by building typed ArrayList<Long>,
ArrayList<Double>, ArrayList<Boolean> or a generic ArrayList<Any?> as
appropriate (preserving nulls), and support nested lists by recursively
converting inner List into ArrayList objects (or ArrayList<Bundle> for nested
maps); ensure empty lists are typed based on provided schema or element probe
(or default to ArrayList<Any?> if unknown) so consumers don't receive an
incorrect StringArrayList; reference the variables result, key, value and the
mapToBundle recursion when implementing these conversions.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`:
- Around line 185-188: parseIntentConfiguration currently throws
IllegalArgumentException when buildIntentConfiguration returns null which will
crash setIntentConfiguration; instead handle nulls/errors like the
customer/appearance parsing path and route failures to onLoadingFailed. Update
parseIntentConfiguration (and/or setIntentConfiguration) to not throw: call
buildIntentConfiguration and if it returns null or throws, create a structured
loading error and invoke onLoadingFailed with that error (mirroring the existing
callback/error object used for customer/appearance parsing), and return null or
noop; ensure you reference parseIntentConfiguration, buildIntentConfiguration,
setIntentConfiguration, and onLoadingFailed so the invalid config from the
Flutter side is reported via onLoadingFailed rather than propagating an uncaught
exception.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`:
- Around line 120-121: Replace the force-cast and unconditional override: safely
attempt to get a mutable dictionary from intentConfiguration (guard/if let for
intentConfiguration.mutableCopy() as? NSMutableDictionary) and bail/return an
error if the cast fails, and only set the "confirmHandler" key to true on
mutableIntentConfig when neither "confirmHandler" nor
"confirmationTokenConfirmHandler" are present in the incoming
intentConfiguration (to avoid clashing with the confirmationTokenConfirmHandler
flow used by StripeSdkImpl+Embedded.swift and causing
createEmbeddedPaymentElement to fail).

In `@packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart`:
- Around line 207-208: StripePlatform currently declares the abstract method
setConfirmHandler(ConfirmHandler? handler) which breaks subclasses like
WebStripe; fix by adding a no-op default implementation on the StripePlatform
class (change the abstract declaration to a concrete method that simply returns
or does nothing) so existing platform implementations don't need to override it,
or alternatively add an explicit override of setConfirmHandler in the WebStripe
class (web_stripe.dart) that is a no-op to satisfy the interface; reference the
StripePlatform class and the setConfirmHandler(ConfirmHandler? handler) method
when making the change.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart`:
- Around line 175-224: The handler can call setState after the State is
disposed; update dispose()/disposeView() to clear the platform callback by
calling _methodChannel?.setMethodCallHandler(null), and inside _handleMethodCall
add an early guard (if (!mounted) return;) before any setState or widget
callback usage (particularly before the onHeightChanged path that calls setState
and widget.onHeightChanged). Ensure this guard runs before processing methods
like 'onHeightChanged' and before invoking other widget callbacks to avoid
post-dispose access.

---

Duplicate comments:
In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`:
- Around line 29-47: The channel.invokeMethod calls in
StripeSdkEmbeddedPaymentElementPlatformView's embeddedView callbacks
(onHeightChanged, onPaymentOptionChanged, onLoadingFailed,
onRowSelectionImmediateAction, onFormSheetConfirmComplete) must be executed on
the Android main thread; update each callback to post the invokeMethod
invocation to the main looper (e.g., use Handler(Looper.getMainLooper()).post {
... } or an equivalent runOnUiThread) so that channel.invokeMethod(...) is
always called from the main thread to avoid threading violations.
- Around line 92-98: The confirm branch sets embeddedView.onConfirmResult but
does not handle synchronous exceptions from viewManager.confirm, which can leak
the callback and hang Flutter; wrap the call to
viewManager.confirm(embeddedView) in try/catch/finally: set
embeddedView.onConfirmResult before the call, in catch call result.error(...)
with the exception details, and in finally clear embeddedView.onConfirmResult =
null to ensure it is never left set; use the same symbols
(embeddedView.onConfirmResult, viewManager.confirm, result.success/result.error)
so the fix is applied to StripeSdkEmbeddedPaymentElementPlatformView.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt`:
- Around line 265-275: The current branch for .rowSelectionBehavior treats a
null initial prop as non-default and installs ImmediateAction; update the
conditional to explicitly check for the Default enum value (and treat null as
Default) so only when type == RowSelectionBehaviorType.Default call
EmbeddedPaymentElement.RowSelectionBehavior.default(), otherwise call
EmbeddedPaymentElement.RowSelectionBehavior.immediateAction { ... }. Locate the
code building rowSelectionBehavior (the variable named type and the call to
.rowSelectionBehavior, and the enum RowSelectionBehaviorType) and change the
condition to explicitly compare type to RowSelectionBehaviorType.Default (or
treat null via a safe coalescing/defaulting expression) before choosing
immediateAction.
- Line 99: The view currently hard-casts context in the reactContext property
(reactContext get() = context as ThemedReactContext) which ties
EmbeddedPaymentElementView to React Native and can crash under other hosts;
change the constructor of EmbeddedPaymentElementView to accept a
ThemedReactContext (or a generic Context) plus injected dependencies: the
StripeSdkModule instance and any event callback lambdas, remove the hard-cast
reactContext property and use the injected references instead, and update all
usages (including the other occurrence where reactContext is referenced) to use
the injected module/callbacks so the view no longer assumes RN-specific context.
- Around line 191-232: The createIntentCallback leaks state and can deadlock
across instances: wrap the await and subsequent cleanup in a try/finally so
keepJsAwakeTask.stop() and stripeSdkModule.embeddedIntentCreationCallback =
CompletableDeferred() always run even if await() throws or is cancelled; replace
the direct await() call with a timed wait (e.g., withTimeoutOrNull) so a missing
JS response returns a CreateIntentResult.Failure instead of hanging; and
eliminate the singleton-per-process collision by routing callbacks per-view
(e.g., include a unique requestId when emitting event and store
CompletableDeferred objects in a map keyed by that id, or document the
single-instance limitation) — update references: createIntentCallback,
KeepJsAwakeTask, stripeSdkModule.embeddedIntentCreationCallback,
stripeSdkModule.eventEmitter.emitOnConfirmHandlerCallback, and
embeddedIntentCreationCallback.await().
- Around line 113-183: The startActivity catch, the requireStripeSdkModule()
catch, and the coroutine await failure paths never return a failed
CustomPaymentMethodResult to Stripe, causing hangs; update the startActivity
exception handler and the requireStripeSdkModule() catch to call
CustomPaymentMethodResultHandler.handleCustomPaymentMethodResult(reactContext,
CustomPaymentMethodResult.failed(displayMessage = "...")) before returning, and
inside the coroutineScope.launch wrap the await call
(stripeSdkModule.customPaymentMethodResultCallback.await()) with a bounded
timeout (e.g., withTimeoutOrNull) and ensure KeepJsAwakeTask.stop() is invoked
in a finally block so it always stops, and on any null/exception path convert to
CustomPaymentMethodResult.failed(...) and pass it to
CustomPaymentMethodResultHandler before finishing and calling
CustomPaymentMethodActivity.finishCurrent().

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`:
- Around line 137-167: The builder chain for
EmbeddedPaymentElement.Configuration in EmbeddedPaymentElementViewManager.kt is
missing the returnURL from the incoming readableMap, causing returnURL to be
dropped on Android; update the configuration building to read
readableMap?.getString("returnURL") and call the corresponding Builder method
(e.g., .returnURL(...)) before .build() so the value is passed through (locate
the EmbeddedPaymentElement.Configuration.Builder(...) block and add the
.returnURL(readableMap?.getString("returnURL")) invocation, preserving
null-safety consistent with other fields like linkConfig and googlePayConfig).
- Around line 97-115: The try/catch blocks in EmbeddedPaymentElementViewManager
(around buildCustomerConfiguration and buildPaymentSheetAppearance) currently
throw a bare Error, losing the original exception and risking an uncaught VM
error; instead catch PaymentSheetException and PaymentSheetAppearanceException
and surface the original exception via view.onLoadingFailed(...) or an RN event
(include the caught exception as the cause/message), then abort or return from
setConfiguration (or guard later usage of customerConfiguration/appearance) so
subsequent code doesn’t run after a parse failure—mirror the behavior used in
PaymentSheetManager for error propagation.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`:
- Around line 190-192: Replace direct use of window?.rootViewController when
setting embeddedElement.presentingViewController with the app's view-controller
traversal helper to avoid presenting behind modals; specifically, in
attachEmbeddedView and embeddedPaymentElementWillPresent locate the presenting
controller via StripeSdkImpl.findViewControllerPresenter() (or the equivalent
function that walks presentedViewController) and assign that controller to
embeddedElement.presentingViewController instead of using
embeddedView.window?.rootViewController so presentation honors active
modal/navigation stacks.
- Around line 120-163: The code stores the created element in a shared singleton
(StripeSdkImpl.shared.embeddedInstance) which allows a second
EmbeddedPaymentElement to overwrite the first and misroute confirm/clear calls;
change createEmbeddedPaymentElement usage in EmbeddedPaymentElementFactory.swift
so it either (A) rejects creation when StripeSdkImpl.shared.embeddedInstance is
already non-nil/attached by returning an error via the
reject/embeddedPaymentElementLoadingFailed callback (use the same error payload
shape), or (B) refactors the iOS side to track instances per viewId (e.g., keep
a dictionary keyed by viewId inside StripeSdkImpl and update
createEmbeddedPaymentElement, embeddedInstance references, and
handle(confirm/clearPaymentOption) to use the viewId key) so
confirm/clearPaymentOption/handle(...) route to the correct instance instead of
the single shared embeddedInstance.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart`:
- Around line 120-144: Update didUpdateWidget and dispose to correctly propagate
config changes and avoid clobbering global state: in didUpdateWidget (in
addition to controller changes) detect changes to widget.intentConfiguration,
widget.configuration, and widget.intentConfiguration.confirmHandler and call the
native update path (mirror card_field.dart — e.g., invoke the existing
method-channel update or re-send creationParams) and re-run
Stripe.instance.setConfirmHandler when the confirmHandler reference changes; in
dispose, stop unconditionally clearing the global handler by tracking ownership
(add a private field like _installedConfirmHandler or _confirmHandlerOwner set
when you call Stripe.instance.setConfirmHandler(thisHandler)) and only call
Stripe.instance.setConfirmHandler(null) if this widget installed the active
handler, leaving other widgets’ handlers untouched.

---

Nitpick comments:
In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt`:
- Line 13: Remove the redundant explicit import of
EmbeddedPaymentElementViewManager since com.reactnativestripesdk.* already
covers it; locate the import line importing EmbeddedPaymentElementViewManager in
StripeAndroidPlugin.kt and delete that single import so only the wildcard import
remains (no other code changes needed).

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt`:
- Around line 55-67: The custom wrapper class ReadableMap (in package
com.facebook.react.bridge) shadows React Native's ReadableMap interface; rename
the wrapper to a distinct name (e.g., RNReadableMap or ReadableMapWrapper) and
update all usages: replace constructor calls in
StripeSdkEmbeddedPaymentElementPlatformView (where ReadableMap(configMap as
Map<String, Any>) and ReadableMap(intentConfigMap as Map<String, Any>) are
used), and update any other references across the codebase (including the
wrapper class file and imports) so parseRowSelectionBehavior,
parseElementConfiguration, and other call sites use the new wrapper type and
imports to avoid name collision with RN's interface.

In `@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe`
Sdk/StripeSdkImpl+Embedded.swift:
- Around line 58-68: Replace the verbose nil-or-empty checks with concise
empty-or-nil checks: for STPAPIClient.shared.publishableKey and
configuration.returnURL use a single condition that treats nil as empty (e.g.
coalesce to "" or use a helper like isNilOrEmpty) so the ifs become simpler and
equivalent; update the branches around STPAPIClient.shared.publishableKey and
configuration.returnURL in StripeSdkImpl+Embedded.swift (the
resolve(Errors.createError(...)) calls and returns remain unchanged).

In `@packages/stripe/lib/src/stripe.dart`:
- Around line 571-575: The docstring for setConfirmHandler should mention that
passing null unregisters the native callback so callers can clear it (e.g., on
dispose) to avoid retaining stale closures; update the comment on void
setConfirmHandler(ConfirmHandler? handler) (and mention ConfirmHandler and
_platform.setConfirmHandler) to explicitly state the clear-handler semantics and
recommend calling setConfirmHandler(null) when disposing EmbeddedPaymentElement.

In `@packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart`:
- Around line 3-53: The controller currently extends ChangeNotifier but never
calls notifyListeners and its methods confirm, clearPaymentOption, and
disposeView only assert attachment (stripped in release), so update
EmbeddedPaymentElementController: call notifyListeners() at the end of
attach(EmbeddedPaymentElementContext), detach(EmbeddedPaymentElementContext) and
override dispose() (notifyListeners before clearing _context) to inform
listeners of state changes; replace the assert guards in confirm(),
clearPaymentOption(), and disposeView() with runtime checks that throw a
StateError with a clear message (e.g., "Controller must be attached to an
EmbeddedPaymentElement") when _context is null so misuse fails loudly in
production; keep using _context?.method() only after verifying non-null.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart`:
- Line 302: The hard-coded fallback of 400 in the EmbeddedPaymentElement causes
clipping before native onHeightChanged reports; update the widget to accept an
optional initialHeight parameter (e.g., add final double? initialHeight to the
EmbeddedPaymentElement constructor) and change the height decision from
"_currentHeight > 0 ? _currentHeight : 400" to use the initialHeight or an
unconstrained alternative (e.g., "_currentHeight > 0 ? _currentHeight :
(widget.initialHeight ?? double.infinity)"), or document callers should wrap in
a parent with constraints if double.infinity is used; ensure any platform view
container code that references _currentHeight uses this new fallback so initial
render won't be clipped.
- Around line 176-224: The blanket try/catch around the platform method dispatch
swallows real errors; change the catch block in the method that processes `call`
(the handler containing switch on call.method) to forward unexpected exceptions
to the consumer via `widget.onLoadingFailed` instead of only calling debugPrint:
capture the caught exception and convert it into the same error type returned by
`_parseLoadingError` (or construct a simple loading error wrapper) and invoke
`widget.onLoadingFailed?.call(...)`, then still log the error with debugPrint;
leave regular per-case error handling (e.g. parsing for
`onPaymentOptionChanged`, `onHeightChanged`) intact so parse/serialization
failures are surfaced to callers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97b88af6-b339-4c4c-8606-51d0d3186e6c

📥 Commits

Reviewing files that changed from the base of the PR and between 14b785f and d14c156.

📒 Files selected for processing (25)
  • packages/stripe/lib/flutter_stripe.dart
  • packages/stripe/lib/src/stripe.dart
  • packages/stripe/lib/src/widgets/embedded_payment_element.dart
  • packages/stripe/lib/src/widgets/embedded_payment_element_controller.dart
  • packages/stripe_android/android/build.gradle
  • packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformViewFactory.kt
  • packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentElementConfig.kt
  • packages/stripe_ios/ios/stripe_ios/Package.resolved
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/EmbeddedPaymentElementView.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+Embedded.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+PaymentSheet.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swift
  • packages/stripe_platform_interface/lib/src/method_channel_stripe.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart
  • packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart
✅ Files skipped from review due to trivial changes (4)
  • packages/stripe/lib/flutter_stripe.dart
  • packages/stripe_android/android/gradle/wrapper/gradle-wrapper.properties
  • packages/stripe_ios/ios/stripe_ios/Package.resolved
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/stripe_android/android/build.gradle
  • packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swift
  • packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentElementConfig.kt
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart
  • packages/stripe_platform_interface/lib/src/models/payment_sheet.dart

Comment on lines +85 to +106
is List<*> -> {
val list = value as List<*>
if (list.isEmpty()) {
result.putStringArrayList(key, ArrayList())
} else {
when (list.first()) {
is String -> {
@Suppress("UNCHECKED_CAST")
result.putStringArrayList(key, ArrayList(list as List<String>))
}
is Int -> {
@Suppress("UNCHECKED_CAST")
result.putIntegerArrayList(key, ArrayList(list as List<Int>))
}
else -> {
android.util.Log.e("mapToBundle", "Cannot put arrays of objects into bundles. Failed on: $key.")
}
}
}
}
else -> android.util.Log.e("mapToBundle", "Could not convert object with key: $key, type: ${value::class.java}")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

mapToBundle silently drops several value shapes the embedded element may send.

A few gaps that will bite at runtime when more complex Flutter creationParams flow through:

  1. List of Map (objects) is unsupported — nested dictionaries inside arrays (e.g., customPaymentMethods, summaryItems, cartItems) silently become an error log and the key is never inserted in the Bundle.
  2. Lists of Long, Double, Boolean, or mixed‑null entries also fall through to the else branch. Even empty lists always become an empty StringArrayList, which may break the consumer if they expect an integer/double list.
  3. Nested List<List<…>> isn't handled.

Given the embedded element passes large serialized configs, consider at minimum handling List<Map>ArrayList<Bundle> and widening the primitive branches, or switch to an established (R)eactNative‑style Bundle converter (React Native's Arguments.toBundle(ReadableMap) is what the rest of the module relies on).

♻️ Sketch
             is List<*> -> {
                 val list = value as List<*>
                 if (list.isEmpty()) {
-                    result.putStringArrayList(key, ArrayList())
+                    // Can't infer element type from empty list; store an empty parcelable-ish list.
+                    result.putStringArrayList(key, ArrayList())
                 } else {
-                    when (list.first()) {
+                    when (val first = list.firstOrNull { it != null }) {
                         is String -> {
                             `@Suppress`("UNCHECKED_CAST")
                             result.putStringArrayList(key, ArrayList(list as List<String>))
                         }
                         is Int -> {
                             `@Suppress`("UNCHECKED_CAST")
                             result.putIntegerArrayList(key, ArrayList(list as List<Int>))
                         }
+                        is Map<*, *> -> {
+                            val bundles = ArrayList<android.os.Bundle>(list.size)
+                            for (item in list) {
+                                `@Suppress`("UNCHECKED_CAST")
+                                bundles.add(mapToBundle(item as? Map<String?, Any?>))
+                            }
+                            result.putParcelableArrayList(key, bundles)
+                        }
                         else -> {
-                            android.util.Log.e("mapToBundle", "Cannot put arrays of objects into bundles. Failed on: $key.")
+                            android.util.Log.e("mapToBundle", "Unsupported list element type ${first?.let { it::class.java }} for key: $key.")
                         }
                     }
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.kt`
around lines 85 - 106, The current mapToBundle branch for List handling drops or
mis-types many element shapes; update the List<*> branch in mapToBundle to
inspect element types and convert accordingly: when elements are Map (e.g.,
Map<String, Any>) convert each to a Bundle via the existing mapToBundle
recursion and put an ArrayList<Bundle> into result; handle Long, Double, Boolean
and nullable/mixed-typed lists by building typed ArrayList<Long>,
ArrayList<Double>, ArrayList<Boolean> or a generic ArrayList<Any?> as
appropriate (preserving nulls), and support nested lists by recursively
converting inner List into ArrayList objects (or ArrayList<Bundle> for nested
maps); ensure empty lists are typed based on provided schema or element probe
(or default to ArrayList<Any?> if unknown) so consumers don't receive an
incorrect StringArrayList; reference the variables result, key, value and the
mapToBundle recursion when implementing these conversions.

Comment on lines +185 to +188
internal fun parseIntentConfiguration(readableMap: ReadableMap?): PaymentSheet.IntentConfiguration {
val intentConfig = buildIntentConfiguration(readableMap)
return intentConfig ?: throw IllegalArgumentException("IntentConfiguration is null")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Parse-time IllegalArgumentException will crash setIntentConfiguration.

parseIntentConfiguration throws IllegalArgumentException on a null result; since setIntentConfiguration (Line 72) doesn't wrap this call, an invalid config from the Flutter side will propagate as an uncaught exception instead of flowing through onLoadingFailed. Mirror the error-path treatment used for customer/appearance parsing (once it's converted to the callback pattern above) and emit a structured loading error instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.kt`
around lines 185 - 188, parseIntentConfiguration currently throws
IllegalArgumentException when buildIntentConfiguration returns null which will
crash setIntentConfiguration; instead handle nulls/errors like the
customer/appearance parsing path and route failures to onLoadingFailed. Update
parseIntentConfiguration (and/or setIntentConfiguration) to not throw: call
buildIntentConfiguration and if it returns null or throws, create a structured
loading error and invoke onLoadingFailed with that error (mirroring the existing
callback/error object used for customer/appearance parsing), and return null or
noop; ensure you reference parseIntentConfiguration, buildIntentConfiguration,
setIntentConfiguration, and onLoadingFailed so the invalid config from the
Flutter side is reported via onLoadingFailed rather than propagating an uncaught
exception.

Comment on lines +120 to +121
let mutableIntentConfig = intentConfiguration.mutableCopy() as! NSMutableDictionary
mutableIntentConfig["confirmHandler"] = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Forced unwrap of mutableCopy() and unconditional confirmHandler = true override.

  1. intentConfiguration.mutableCopy() as! NSMutableDictionary is a force cast; while NSDictionary.mutableCopy() does return NSMutableDictionary, prefer a safe cast with an early failure path so a future refactor doesn't crash the app.
  2. Unconditionally setting confirmHandler = true overrides whatever was sent from Dart. If a caller is using (or intends to use) confirmationTokenConfirmHandler, StripeSdkImpl+Embedded.swift lines 34–37 reject when both are present, which will make createEmbeddedPaymentElement fail with a confusing error. Consider only forcing confirmHandler when neither handler is set in the payload, or document clearly that only the confirmHandler flow is supported on Flutter.
🔧 Proposed adjustment
-        let mutableIntentConfig = intentConfiguration.mutableCopy() as! NSMutableDictionary
-        mutableIntentConfig["confirmHandler"] = true
+        guard let mutableIntentConfig = intentConfiguration.mutableCopy() as? NSMutableDictionary else {
+            channel.invokeMethod("embeddedPaymentElementLoadingFailed",
+                                 arguments: ["message": "Invalid intentConfiguration"])
+            return
+        }
+        if mutableIntentConfig["confirmationTokenConfirmHandler"] == nil {
+            mutableIntentConfig["confirmHandler"] = true
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swift`
around lines 120 - 121, Replace the force-cast and unconditional override:
safely attempt to get a mutable dictionary from intentConfiguration (guard/if
let for intentConfiguration.mutableCopy() as? NSMutableDictionary) and
bail/return an error if the cast fails, and only set the "confirmHandler" key to
true on mutableIntentConfig when neither "confirmHandler" nor
"confirmationTokenConfirmHandler" are present in the incoming
intentConfiguration (to avoid clashing with the confirmationTokenConfirmHandler
flow used by StripeSdkImpl+Embedded.swift and causing
createEmbeddedPaymentElement to fail).

Comment on lines +207 to +208
/// Set the confirm handler for embedded payment elements
void setConfirmHandler(ConfirmHandler? handler);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether stripe_web's WebStripe subclass overrides setConfirmHandler in this branch.
fd -t f 'stripe_web\.dart$' packages/stripe_web
rg -nP 'class\s+WebStripe\b' --type=dart -C2
rg -nP '\bsetConfirmHandler\s*\(' --type=dart -C2

Repository: flutter-stripe/flutter_stripe

Length of output: 3050


🏁 Script executed:

#!/bin/bash
# Check the full WebStripe class and look for setConfirmHandler override
cat -n packages/stripe_web/lib/src/web_stripe.dart | head -100

# Search more broadly for setConfirmHandler in the entire web package
echo "=== Searching for setConfirmHandler in web package ==="
rg 'setConfirmHandler' packages/stripe_web/ -n

Repository: flutter-stripe/flutter_stripe

Length of output: 3975


🏁 Script executed:

#!/bin/bash
# Check the total size of web_stripe.dart and search more comprehensively
wc -l packages/stripe_web/lib/src/web_stripe.dart

# Search for all `@override` methods in WebStripe to confirm scope
rg '@override' packages/stripe_web/lib/src/web_stripe.dart -A1 | head -50

Repository: flutter-stripe/flutter_stripe

Length of output: 1342


🏁 Script executed:

#!/bin/bash
# Count total `@override` methods in WebStripe
echo "Total `@override` methods in WebStripe:"
rg '@override' packages/stripe_web/lib/src/web_stripe.dart | wc -l

# Check if setConfirmHandler exists anywhere in the file
echo ""
echo "Searching for 'setConfirmHandler' in web_stripe.dart:"
rg 'setConfirmHandler' packages/stripe_web/lib/src/web_stripe.dart

Repository: flutter-stripe/flutter_stripe

Length of output: 169


Breaking change for third-party platform implementations (e.g., WebStripe).

Adding setConfirmHandler as an abstract method with no default implementation forces every subclass of StripePlatform to provide a concrete override. WebStripe in packages/stripe_web currently lacks this override and fails to compile with Missing concrete implementation of 'StripePlatform.setConfirmHandler'. Either:

  • provide a no-op default body here (since the confirm handler is only meaningful when a native embedded element exists), or
  • add the override in packages/stripe_web/lib/src/web_stripe.dart as part of this PR so the web package continues to build.
🛠️ Option A — default no-op on the interface
   /// Set the confirm handler for embedded payment elements
-  void setConfirmHandler(ConfirmHandler? handler);
+  void setConfirmHandler(ConfirmHandler? handler) {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Set the confirm handler for embedded payment elements
void setConfirmHandler(ConfirmHandler? handler);
/// Set the confirm handler for embedded payment elements
void setConfirmHandler(ConfirmHandler? handler) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart`
around lines 207 - 208, StripePlatform currently declares the abstract method
setConfirmHandler(ConfirmHandler? handler) which breaks subclasses like
WebStripe; fix by adding a no-op default implementation on the StripePlatform
class (change the abstract declaration to a concrete method that simply returns
or does nothing) so existing platform implementations don't need to override it,
or alternatively add an explicit override of setConfirmHandler in the WebStripe
class (web_stripe.dart) that is a no-op to satisfy the interface; reference the
StripePlatform class and the setConfirmHandler(ConfirmHandler? handler) method
when making the change.

Comment on lines +175 to +224
Future<void> _handleMethodCall(MethodCall call) async {
try {
switch (call.method) {
case 'onPaymentOptionChanged':
final arguments = call.arguments as Map?;
if (arguments != null) {
final paymentOptionMap = Map<String, dynamic>.from(
arguments['paymentOption'] ?? {},
);
if (paymentOptionMap.isNotEmpty) {
final paymentOption = PaymentSheetPaymentOption.fromJson(
paymentOptionMap,
);
widget.onPaymentOptionChanged?.call(paymentOption);
} else {
widget.onPaymentOptionChanged?.call(null);
}
}
break;
case 'onHeightChanged':
final arguments = call.arguments as Map?;
if (arguments != null) {
final height = (arguments['height'] as num?)?.toDouble() ?? 0;
if (height <= 0) return;

setState(() {
_currentHeight = height;
});
widget.onHeightChanged?.call(height);
}
break;
case 'embeddedPaymentElementLoadingFailed':
final error = _parseLoadingError(call.arguments);
widget.onLoadingFailed?.call(error);
break;
case 'embeddedPaymentElementFormSheetConfirmComplete':
final arguments = call.arguments as Map?;
if (arguments != null) {
final result = Map<String, dynamic>.from(arguments);
widget.onFormSheetConfirmComplete?.call(result);
}
break;
case 'embeddedPaymentElementRowSelectionImmediateAction':
widget.onRowSelectionImmediateAction?.call();
break;
}
} catch (e) {
debugPrint('Error handling method call ${call.method}: $e');
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard setState against post-dispose method-channel callbacks.

dispose() doesn't call _methodChannel?.setMethodCallHandler(null), so if a late platform callback arrives after the state is torn down (or during the disposeView()/end-of-frame window where _methodChannel is still live), onHeightChanged path at Line 200 will hit setState on an unmounted State and throw. Clear the handler in dispose/disposeView and guard with if (!mounted) return; inside _handleMethodCall.

🔧 Suggested change
   `@override`
   void dispose() {
+    _methodChannel?.setMethodCallHandler(null);
+    _methodChannel = null;
     controller.detach(this);
     _fallbackController?.dispose();
     if (widget.intentConfiguration.confirmHandler != null) {
       Stripe.instance.setConfirmHandler(null);
     }
     super.dispose();
   }
 …
   Future<void> _handleMethodCall(MethodCall call) async {
+    if (!mounted) return;
     try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stripe/lib/src/widgets/embedded_payment_element.dart` around lines
175 - 224, The handler can call setState after the State is disposed; update
dispose()/disposeView() to clear the platform callback by calling
_methodChannel?.setMethodCallHandler(null), and inside _handleMethodCall add an
early guard (if (!mounted) return;) before any setState or widget callback usage
(particularly before the onHeightChanged path that calls setState and
widget.onHeightChanged). Ensure this guard runs before processing methods like
'onHeightChanged' and before invoking other widget callbacks to avoid
post-dispose access.

@remonh87 remonh87 merged commit 3438f09 into flutter-stripe:main Apr 28, 2026
3 of 6 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 20, 2026
@coderabbitai coderabbitai Bot mentioned this pull request May 29, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Awaiting response Awaiting response from the issuer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants