Add EmbeddedPaymentElement support for Flutter#2239
Add EmbeddedPaymentElement support for Flutter#2239programmeraditya wants to merge 17 commits intoflutter-stripe:mainfrom
Conversation
06e9f2f to
145e5d4
Compare
|
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 |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
What command did you run for dart format?
Looks like quite some changes were made due formatter
| @override | ||
| void setConfirmHandler(ConfirmHandler? handler) { | ||
| _confirmHandler = handler; | ||
| } | ||
|
|
There was a problem hiding this comment.
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?
|
Hello @remonh87 |
|
@jonasbark can you take a look at it as well? |
|
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 |
|
@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 |
…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.
…to IntentConfiguration
📝 WalkthroughWalkthroughThis PR introduces support for Stripe's Embedded Payment Element in Flutter, adding new public widget and controller APIs, platform-specific implementations for Android and iOS with native platform views, and updates to the platform interface to support confirm handler registration and payment method configuration IDs. Changes
Sequence DiagramsequenceDiagram
participant FlutterApp as Flutter App
participant Widget as EmbeddedPaymentElement<br/>Widget
participant Channel as MethodChannel
participant PlatformView as Native Platform View<br/>(Android/iOS)
participant StripeSDK as Stripe SDK
participant UIUser as User
FlutterApp->>Widget: Create with IntentConfiguration
Widget->>Widget: Attach Controller
Widget->>Channel: Initialize Platform View
Channel->>PlatformView: Create View
PlatformView->>StripeSDK: Configure Element
StripeSDK-->>PlatformView: Element Ready
PlatformView-->>Channel: onHeightChanged
Channel-->>Widget: Callback
Widget-->>FlutterApp: onHeightChanged
UIUser->>PlatformView: Select Payment Option
PlatformView-->>Channel: onPaymentOptionChanged
Channel-->>Widget: Callback
Widget-->>FlutterApp: onPaymentOptionChanged
FlutterApp->>Widget: Call confirm()
Widget->>Channel: confirm (RPC)
Channel->>PlatformView: confirm
PlatformView->>StripeSDK: Confirm Payment
StripeSDK-->>PlatformView: Result
PlatformView-->>Channel: Return Result
Channel-->>Widget: Result
Widget-->>FlutterApp: Return Result
FlutterApp->>Widget: Dispose
Widget->>Channel: disposeView()
Channel->>PlatformView: Cleanup
PlatformView-->>Channel: Disposed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@remonh87 the code is up to date with main. Also, the |
There was a problem hiding this comment.
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
FlatConfigmust also have@JsonSerializable(explicitToJson: true)to properly serialize nested configs.When
RowConfig.toJson()is called (which hasexplicitToJson: true), it invokesflat?.toJson(). However,FlatConfig.toJson()currently does not recursively serialize its nested custom objects (separatorInsets,radio,checkmark,disclosure). WithoutexplicitToJson: true, the generated_$FlatConfigToJsonassigns these as raw model objects rather than calling their.toJson()methods. SinceStandardMessageCodeconly accepts primitives, lists, and maps, the platform-view creation will fail when encodingrow.flat.*customizations. Add@JsonSerializable(explicitToJson: true)toFlatConfig: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
StripePlatformwill 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
assertfollowed by_context?.confirm()creates inconsistent behavior:
- Debug mode: Assert fires, throws if not attached
- Release mode: Assert stripped, silently returns
nullFor 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
📒 Files selected for processing (24)
packages/stripe/lib/flutter_stripe.dartpackages/stripe/lib/src/stripe.dartpackages/stripe/lib/src/widgets/embedded_payment_element.dartpackages/stripe/lib/src/widgets/embedded_payment_element_controller.dartpackages/stripe_android/android/build.gradlepackages/stripe_android/android/gradle/wrapper/gradle-wrapper.propertiespackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.ktpackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.ktpackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformViewFactory.ktpackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkModuleExtensions.ktpackages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.ktpackages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementViewManager.ktpackages/stripe_ios/ios/stripe_ios/Package.resolvedpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/EmbeddedPaymentElementFactory.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/EmbeddedPaymentElementView.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+Embedded.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl+PaymentSheet.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/StripeSdkImpl.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swiftpackages/stripe_platform_interface/lib/src/method_channel_stripe.dartpackages/stripe_platform_interface/lib/src/models/payment_sheet.dartpackages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dartpackages/stripe_platform_interface/lib/src/models/payment_sheet.g.dartpackages/stripe_platform_interface/lib/src/stripe_platform_interface.dart
| composeOptions { | ||
| kotlinCompilerExtensionVersion = '1.5.1' | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/stripe_android/android/build.gradle | head -100Repository: 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.gradleRepository: 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:
- 1: https://developer.android.com/jetpack/androidx/releases/compose-kotlin
- 2: https://kotlinlang.org/docs/compose-compiler-migration-guide.html
- 3: https://kotlinlang.org/docs/multiplatform/compose-compiler.html
- 4: https://kotlinlang.org/docs/whatsnew20.html
- 5: https://developer.android.com/develop/ui/compose/setup-compose-dependencies-and-compiler
- 6: https://developer.android.com/develop/ui/compose/bom
- 7: https://developer.android.com/jetpack/androidx/releases/compose
- 8: https://developer.android.com/jetpack/androidx/releases/compose-compiler
- 9: https://developer.android.com/jetpack/androidx/releases/compose-runtime
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.
| 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.
| // 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' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the actual build.gradle file in the repository
fd -e build.gradle | head -20Repository: 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:
- 1: https://index.scala-lang.org/stripe/stripe-android
- 2: https://github.com/stripe/stripe-android
- 3: https://github.com/stripe/stripe-android/blob/master/MIGRATING.md
- 4: https://github.com/stripe/stripe-android/blob/master/CHANGELOG.md
🏁 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 -10Repository: 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:
- 1: https://developer.android.com/develop/ui/compose/bom/bom-mapping
- 2: https://mvnrepository.com/artifact/androidx.compose/compose-bom/2023.10.01
- 3: https://developer.android.com/develop/ui/compose/bom
- 4: http://android-dot-devsite-v2-prod.appspot.com/develop/ui/compose/bom/bom-mapping
🏁 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:
- 1: https://developer.android.com/develop/ui/compose/bom/bom-mapping
- 2: https://android-dot-devsite-v2-prod.appspot.com/develop/ui/compose/bom/bom-mapping
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧩 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=ktRepository: flutter-stripe/flutter_stripe
Length of output: 99
🏁 Script executed:
fd -e kt | head -20Repository: flutter-stripe/flutter_stripe
Length of output: 1999
🏁 Script executed:
cat -n packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkEmbeddedPaymentElementPlatformView.ktRepository: 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 -B2Repository: 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 -A2Repository: 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:
- 1: https://docs.flutter.dev/platform-integration/platform-channels
- 2: https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodChannel.MethodCallHandler.html
- 3: https://flutter.dev/platform-channels/
- 4: InvokeMethod issue on Android side, MethodChannel and EventChannel must be executed on the main thread flutter/flutter#36051
- 5: https://newdevpoint.in/which-thread-methodchannel-use/
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).
| "confirm" -> { | ||
| embeddedView.onConfirmResult = { resultMap -> | ||
| result.success(resultMap) | ||
| embeddedView.onConfirmResult = null | ||
| } | ||
| viewManager.confirm(embeddedView) | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk && \
ls -la | head -20Repository: flutter-stripe/flutter_stripe
Length of output: 1469
🏁 Script executed:
wc -l packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.ktRepository: flutter-stripe/flutter_stripe
Length of output: 179
🏁 Script executed:
cat -n packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/EmbeddedPaymentElementView.ktRepository: flutter-stripe/flutter_stripe
Length of output: 20397
🏁 Script executed:
cat -n packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CustomPaymentMethodActivity.ktRepository: flutter-stripe/flutter_stripe
Length of output: 3219
🏁 Script executed:
rg "CustomPaymentMethodResult" packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/ -A 3 -B 3Repository: 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, sendCustomPaymentMethodResult.failed() - Line 134: If
requireStripeSdkModule()fails, sendCustomPaymentMethodResult.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.
| if let viewController = embeddedView.window?.rootViewController { | ||
| embeddedElement.presentingViewController = viewController | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the EmbeddedPaymentElementFactory.swift file
find . -name "EmbeddedPaymentElementFactory.swift" -type fRepository: 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.swiftRepository: 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.swiftRepository: 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 2Repository: 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.swiftRepository: flutter-stripe/flutter_stripe
Length of output: 1760
🏁 Script executed:
# Find the findViewControllerPresenter function
rg "func findViewControllerPresenter" -A 15Repository: 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 3Repository: 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -type f -name "EmbeddedPaymentElementView.swift" 2>/dev/null | head -20Repository: flutter-stripe/flutter_stripe
Length of output: 55
🏁 Script executed:
cd /tmp && find . -type f -name "*EmbeddedPaymentElement*" 2>/dev/null | head -20Repository: 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/nullRepository: 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 -20Repository: flutter-stripe/flutter_stripe
Length of output: 360
🏁 Script executed:
# Check current working directory
pwd && ls -laRepository: flutter-stripe/flutter_stripe
Length of output: 1274
🏁 Script executed:
find / -type f -name "EmbeddedPaymentElementView.swift" 2>/dev/null | head -5Repository: 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 -20Repository: 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 -30Repository: flutter-stripe/flutter_stripe
Length of output: 821
🏁 Script executed:
# Search for usage of presentingViewController
rg "presentingViewController" --type swift 2>/dev/null | head -50Repository: flutter-stripe/flutter_stripe
Length of output: 2785
🏁 Script executed:
# Search for window?.rootViewController usage pattern
rg "window\?" --type swift 2>/dev/null | head -30Repository: 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 -40Repository: 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 5Repository: 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.
| StripeSdkImpl.shared.intentCreationCallback( | ||
| result: params, | ||
| resolver: resolver(for: result), | ||
| rejecter: rejecter(for: result) | ||
| ) | ||
| result(nil) |
There was a problem hiding this comment.
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.
| @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); |
There was a problem hiding this comment.
🧩 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 20Repository: 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.
| if (widget.intentConfiguration.confirmHandler != null) { | ||
| Stripe.instance.setConfirmHandler( | ||
| widget.intentConfiguration.confirmHandler!, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd embedded_payment_element.dartRepository: flutter-stripe/flutter_stripe
Length of output: 134
🏁 Script executed:
cat -n packages/stripe/lib/src/widgets/embedded_payment_element.dart | head -300Repository: 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.dartRepository: flutter-stripe/flutter_stripe
Length of output: 1448
🏁 Script executed:
fd -e dart | xargs grep -l "EmbeddedPaymentElement" | head -10Repository: flutter-stripe/flutter_stripe
Length of output: 511
🏁 Script executed:
# Look for the platform view implementations
rg "_AndroidEmbeddedPaymentElement|_UiKitEmbeddedPaymentElement" -A 15Repository: 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 5Repository: 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 2Repository: 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.dartRepository: 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 -50Repository: 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 -100Repository: 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 3Repository: 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 -40Repository: 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 -50Repository: 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.dartRepository: 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.dartRepository: 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 -5Repository: 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 -5Repository: 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 20Repository: 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.dartRepository: 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.
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
EmbeddedPaymentElementwidget with iOS/Android platform viewsEmbeddedPaymentElementControllerto callconfirm()andclearPaymentOption()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
React Native docs: https://docs.stripe.com/payments/mobile/accept-payment-embedded?platform=react-native
Summary by CodeRabbit
Release Notes
setConfirmHandlerAPI for registering custom payment confirmation handlers