feat: add PaymentMethodMessaging widget (via PaymentMethodMessagingElement)#2403
feat: add PaymentMethodMessaging widget (via PaymentMethodMessagingElement)#2403realmeylisdev wants to merge 5 commits into
Conversation
Freezed model for Stripe's PaymentMethodMessagingElement (Klarna, Afterpay/Clearpay, Affirm). Includes optional locale to match the Element API surface on both iOS and Android.
Platform-view widget that renders Stripe's promotional messaging element for Klarna/Afterpay/Affirm. Ships on iOS and Android via the PaymentMethodMessagingElement preview API; throws UnsupportedError elsewhere. Native side reports height changes via a per-viewId MethodChannel; didUpdateWidget forwards configuration changes through updateConfiguration; dispose unregisters the handler.
- PaymentMethodMessagingElementView (under Stripe Sdk/) is a plain UIView that owns a Stripe SDK PaymentMethodMessagingElement, kicks off create(configuration:) on an @mainactor Task, embeds element.view with Auto Layout on success, and reports height changes via a callback. Guards against a missing publishable key and collapses to zero height on noContent / failed results. - PaymentMethodMessagingElementFactory is a thin FlutterPlatformView adapter — per-viewId FlutterMethodChannel for updateConfiguration / onHeightChange, NSNumber-based amount decode for the Flutter codec. - Register the factory in StripePlugin with viewId flutter.stripe/payment_method_messaging. - Update Package.resolved to the 25.9.0 SPM revision that matches Package.swift's exact pin (main was committed with a stale resolve). Uses the `@_spi(PaymentMethodMessagingElementPreview) @_spi(STP) import StripePaymentSheet` import that the element is gated behind.
…m view - Add the separate com.stripe:payment-method-messaging artifact dependency, pinned to the same stripe_version as stripe-android. - StripePaymentMethodMessagingPlatformViewFactory mirrors the existing Aubecs/CardField factories, including the Map<String?, Any?>? creationParams type used across the repo. - StripePaymentMethodMessagingPlatformView hosts an AndroidX ComposeView inside a FrameLayout, calls PaymentMethodMessagingElement.create(application) + configure(config) from a scope-tied coroutine, swaps composition state on result (Succeeded → render element.Content(appearance); NoContent/Failed → empty composition + collapsed height). Height changes are reported in dp via Modifier.onSizeChanged. dispose() cancels the scope, disposes composition, and clears the method-channel handler. - Register the factory in StripeAndroidPlugin with viewId flutter.stripe/payment_method_messaging. Uses Stripe's @OptIn(PaymentMethodMessagingElementPreview::class) preview annotation.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a PaymentMethodMessaging Flutter widget and model, implements native platform views on Android and iOS (Compose-backed Android view and UIView-backed iOS view), wires method-channel configuration/height updates, and exposes the widget via package exports. Changes
Sequence DiagramsequenceDiagram
participant Flutter as Flutter Widget
participant MethodCh as Method Channel
participant Android as Android PlatformView
participant Stripe as Stripe Element
participant DartCB as Dart Callback
Flutter->>Android: create platform view (configuration)
Android->>Stripe: create element (Application)
activate Stripe
Stripe-->>Android: element created
deactivate Stripe
Android->>Stripe: configure(configuration) [suspend]
activate Stripe
Stripe-->>Android: ConfigureResult (Succeeded/NoContent/Failed)
deactivate Stripe
Android->>Android: render element.Content() (Compose)
Android->>MethodCh: invokeMethod("onHeightChange", {"height": h})
MethodCh->>DartCB: onHeightChange callback invoked
Flutter->>MethodCh: updateConfiguration(newConfig)
MethodCh->>Android: receiveMethodCall("updateConfiguration")
Android->>Stripe: configure(newConfig)
activate Stripe
Stripe-->>Android: ConfigureResult
deactivate Stripe
Android->>MethodCh: invokeMethod("onHeightChange", {"height": newH})
MethodCh->>DartCB: onHeightChange callback invoked
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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`:
- Line 67: The dependency declaration implementation
"com.stripe:payment-method-messaging:$stripe_version" references a non-existent
23.1.x train; replace the dynamic/incorrect version usage by pinning this
artifact to a known published version (e.g., 21.19.0) or set a valid explicit
version range that exists on Maven Central so Gradle resolution succeeds; update
the implementation line for com.stripe:payment-method-messaging and, if
necessary, adjust the stripe_version variable or use a hardcoded version string
to ensure PaymentMethodMessagingElement is provided.
In
`@packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformView.kt`:
- Around line 45-77: The element state is declared inside composeView.setContent
which can be null when applyConfiguration runs; move the state to a class-level
Compose MutableState so updates from applyConfiguration aren't lost. Concretely:
replace the local "var element by remember {
mutableStateOf<PaymentMethodMessagingElement?>(null) }" and the function
property "private var elementState: ((PaymentMethodMessagingElement?) -> Unit)?
= null" with a class-level "private val elementState =
mutableStateOf<PaymentMethodMessagingElement?>(null)" (or similar MutableState
variable), update composeView.setContent to read elementState.value (and call
elementState.value = ... instead of invoking a lambda), and update
applyConfiguration to set elementState.value when configuring the element; keep
lastReportedHeightPx and the onSizeChanged logic as-is.
🪄 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: 011bac51-83b3-4cde-898f-98476ca88c6d
📒 Files selected for processing (14)
packages/stripe/lib/flutter_stripe.dartpackages/stripe/lib/src/widgets/payment_method_messaging.dartpackages/stripe_android/android/build.gradlepackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.ktpackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformView.ktpackages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformViewFactory.ktpackages/stripe_ios/ios/stripe_ios/Package.resolvedpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/PaymentMethodMessagingElementFactory.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/Stripe Sdk/PaymentMethodMessagingElementView.swiftpackages/stripe_ios/ios/stripe_ios/Sources/stripe_ios/StripePlugin.swiftpackages/stripe_platform_interface/lib/src/models/payment_method_messaging.dartpackages/stripe_platform_interface/lib/src/models/payment_method_messaging.freezed.dartpackages/stripe_platform_interface/lib/src/models/payment_method_messaging.g.dartpackages/stripe_platform_interface/lib/stripe_platform_interface.dart
| implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" | ||
| implementation "com.stripe:stripe-android:$stripe_version" | ||
| implementation "com.stripe:financial-connections:$stripe_version" | ||
| implementation "com.stripe:payment-method-messaging:$stripe_version" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# List published versions of com.stripe:payment-method-messaging on Maven Central
# and show the newest version in the 23.1.x train, if any.
curl -s 'https://search.maven.org/solrsearch/select?q=g:com.stripe+AND+a:payment-method-messaging&core=gav&rows=50&wt=json' \
| jq -r '.response.docs[] | "\(.v)"' \
| sort -VRepository: flutter-stripe/flutter_stripe
Length of output: 454
Fix version mismatch: com.stripe:payment-method-messaging does not publish 23.1.x versions.
The artifact com.stripe:payment-method-messaging on Maven Central only provides versions up to 21.19.0. There are no versions in the 23.x train or 23.1.x range. Using 23.1.+ will cause Gradle resolution to fail at build time.
Pin this dependency to 21.19.0 or the lowest compatible version that provides PaymentMethodMessagingElement, or align it with a version range that actually exists for this artifact.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/stripe_android/android/build.gradle` at line 67, The dependency
declaration implementation "com.stripe:payment-method-messaging:$stripe_version"
references a non-existent 23.1.x train; replace the dynamic/incorrect version
usage by pinning this artifact to a known published version (e.g., 21.19.0) or
set a valid explicit version range that exists on Maven Central so Gradle
resolution succeeds; update the implementation line for
com.stripe:payment-method-messaging and, if necessary, adjust the stripe_version
variable or use a hardcoded version string to ensure
PaymentMethodMessagingElement is provided.
| private var elementState: ((PaymentMethodMessagingElement?) -> Unit)? = null | ||
| private var lastReportedHeightPx: Int = -1 | ||
|
|
||
| init { | ||
| container.addView( | ||
| composeView, | ||
| FrameLayout.LayoutParams( | ||
| FrameLayout.LayoutParams.MATCH_PARENT, | ||
| FrameLayout.LayoutParams.WRAP_CONTENT, | ||
| ), | ||
| ) | ||
| composeView.setContent { | ||
| var element by remember { mutableStateOf<PaymentMethodMessagingElement?>(null) } | ||
| elementState = { element = it } | ||
| val density = LocalDensity.current | ||
| Box( | ||
| modifier = Modifier.onSizeChanged { size -> | ||
| if (size.height != lastReportedHeightPx) { | ||
| lastReportedHeightPx = size.height | ||
| val heightDp = with(density) { size.height.toDp().value } | ||
| channel.invokeMethod( | ||
| "onHeightChange", | ||
| mapOf("height" to heightDp.toDouble()), | ||
| ) | ||
| } | ||
| }, | ||
| ) { | ||
| element?.Content(PaymentMethodMessagingElement.Appearance()) | ||
| } | ||
| } | ||
|
|
||
| channel.setMethodCallHandler { call, result -> handleMethodCall(call, result) } | ||
| applyConfiguration(creationParams) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformView.kt | head -150Repository: flutter-stripe/flutter_stripe
Length of output: 6860
🏁 Script executed:
# Check the entire file to understand the full context
wc -l packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformView.ktRepository: flutter-stripe/flutter_stripe
Length of output: 187
🏁 Script executed:
# Look for the applyConfiguration method
rg -A 30 "fun applyConfiguration" packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformView.ktRepository: flutter-stripe/flutter_stripe
Length of output: 1405
Move element state to class-level Compose state to avoid race condition.
applyConfiguration(creationParams) runs synchronously after composeView.setContent() returns, but AndroidX ComposeView defers its initial composition until the view is attached or createComposition() is explicitly called. This creates a race where the coroutine in applyConfiguration can complete and call elementState?.invoke(element) before the composition has even started—meaning elementState is still null and the configured element is silently dropped.
Suggested fix
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var configureJob: Job? = null
- private var elementState: ((PaymentMethodMessagingElement?) -> Unit)? = null
+ private var currentElement by mutableStateOf<PaymentMethodMessagingElement?>(null)
private var lastReportedHeightPx: Int = -1
@@
)
composeView.setContent {
- var element by remember { mutableStateOf<PaymentMethodMessagingElement?>(null) }
- elementState = { element = it }
val density = LocalDensity.current
Box(
@@
) {
- element?.Content(PaymentMethodMessagingElement.Appearance())
+ currentElement?.Content(PaymentMethodMessagingElement.Appearance())
}
}
@@
private fun applyConfiguration(params: Map<String?, Any?>?) {
configureJob?.cancel()
- elementState?.invoke(null)
+ currentElement = null
emitCollapsedHeight()
@@
configureJob = scope.launch {
- val element = PaymentMethodMessagingElement.create(application)
- val result = element.configure(configuration)
+ val configuredElement = PaymentMethodMessagingElement.create(application)
+ val result = configuredElement.configure(configuration)
when (result) {
is PaymentMethodMessagingElement.ConfigureResult.Succeeded -> {
- elementState?.invoke(element)
+ currentElement = configuredElement
}
is PaymentMethodMessagingElement.ConfigureResult.NoContent,
is PaymentMethodMessagingElement.ConfigureResult.Failed -> {
- elementState?.invoke(null)
+ currentElement = null
emitCollapsedHeight()
}
}🤖 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/StripePaymentMethodMessagingPlatformView.kt`
around lines 45 - 77, The element state is declared inside
composeView.setContent which can be null when applyConfiguration runs; move the
state to a class-level Compose MutableState so updates from applyConfiguration
aren't lost. Concretely: replace the local "var element by remember {
mutableStateOf<PaymentMethodMessagingElement?>(null) }" and the function
property "private var elementState: ((PaymentMethodMessagingElement?) -> Unit)?
= null" with a class-level "private val elementState =
mutableStateOf<PaymentMethodMessagingElement?>(null)" (or similar MutableState
variable), update composeView.setContent to read elementState.value (and call
elementState.value = ... instead of invoking a lambda), and update
applyConfiguration to set elementState.value when configuring the element; keep
lastReportedHeightPx and the onSizeChanged logic as-is.
payment-method-messaging:23.1.0 declares androidx.compose.foundation, androidx.compose.ui, and androidx.compose.runtime at `runtime` scope in its POM, so Kotlin compilation in this module can't see `Box`, `Modifier`, `remember`, etc. without explicit `implementation` dependencies. Pin the versions to 1.10.4 to match Stripe's own declared dependency graph.
CI state on 6d63683 — ready for reviewGreen on everything the PR controls:
Remaining red is the pre-existing fork-PR secret limitation, not this PR:
One fix went in mid-PR (commit 6d63683): |
Summary
Add a
PaymentMethodMessagingFlutter widget that wraps Stripe'sPaymentMethodMessagingElement(Klarna / Afterpay / Affirm promotional messaging) on both iOS and Android. Supersedes #2387 — see the closing comment there for the full post-mortem on why that branch was abandoned.Why the rewrite
The prior PR #2387 referenced SDK classes that don't exist: iOS
StripePaymentSheet.PaymentMethodMessagingViewand Androidcom.stripe.android.paymentsheet.paymentmethodmessaging.PaymentMethodMessagingView. Neither is published by Stripe at any version. The correct class on both platforms isPaymentMethodMessagingElement, gated behind a preview annotation — the same API thatstripe-react-nativev0.61 wraps.This PR starts clean from
main(which already carries the 25.9.0stripe-ios-spmbump) and implements both platforms against the real API.What changed
stripe_platform_interface): FreezedPaymentMethodMessagingConfigurationwithpaymentMethods,currency,amount, optionalcountryCode, optionallocale. Enum covers Klarna, Afterpay/Clearpay, and Affirm (matches the Element's supported methods).stripe):PaymentMethodMessagingwidget with auto-resizing height,didUpdateWidgetforwarding config changes to native via a per-viewIdMethodChannel,disposeclearing the handler, explicit iOS / Android /UnsupportedErrorbranching.stripe_ios):PaymentMethodMessagingElementViewUIView subclass (underStripe Sdk/, matchingCardFieldView/AuBECSDebitFormView) owns thePaymentMethodMessagingElement, embedselement.viewwith Auto Layout, guards publishable-key presence, emits height viasystemLayoutSizeFittinginlayoutSubviews.PaymentMethodMessagingElementFactoryis the thinFlutterPlatformViewadapter handlingupdateConfigurationandonHeightChangeover the per-viewId channel (withNSNumber.intValuefor the Flutter codec). Registered underflutter.stripe/payment_method_messaginginStripePlugin.stripe_android): adds the separatecom.stripe:payment-method-messaging:$stripe_versionartifact. Platform view hosts an AndroidXComposeViewinside aFrameLayout;applyConfigurationlaunches a coroutine that callsPaymentMethodMessagingElement.create(application).configure(config)and swaps composition state on the result (Succeeded→ renderelement.Content(appearance);NoContent/Failed→ empty composition + collapsed height). Height reported in dp viaModifier.onSizeChanged.dispose()cancels the scope, disposes the composition, clears the method-channel handler.Package.resolvedto match the 25.9.0 pin inPackage.swift— main was committed with a stale resolve.Scope notes
Intentionally deferred to follow-ups (keeps review surface small):
UIFont/UIColorand AndroidTheme/Font/Colorsdon't map cleanly to a shared Dart model; worth a dedicated design pass. Default Stripe appearance works for a first release.loading / loaded / no_content / failed(error)to JS. Here we surface no-content/failed only via collapse-to-zero height. A dedicatedonConfigureResultcallback can land later.Closes
Test plan
dart analyzeclean acrosspackages/stripeandpackages/stripe_platform_interface.dart run build_runner build --delete-conflicting-outputsregenerates Freezed/JSON cleanly.amountin the configuration while mounted — thedidUpdateWidget→updateConfigurationpath reconfigures the native element.UnsupportedErrorat build rather than failing inside Android-specific APIs.Summary by CodeRabbit