Skip to content

Commit 179ff17

Browse files
committed
wip native improvements
1 parent 492b19f commit 179ff17

14 files changed

Lines changed: 806 additions & 241 deletions

packages/stripe/lib/src/widgets/payment_method_messaging.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,22 @@ class _PaymentMethodMessagingState extends State<PaymentMethodMessaging> {
4242
_methodChannel =
4343
MethodChannel('flutter.stripe/payment_method_messaging/$viewId');
4444
_methodChannel!.setMethodCallHandler((call) async {
45-
if (call.method == 'onHeightChange') {
46-
final args = Map<String, dynamic>.from(call.arguments);
47-
final height = (args['height'] as num).toDouble();
48-
if (mounted) {
49-
setState(() {
50-
_height = height;
51-
});
52-
}
53-
widget.onHeightChange?.call(height);
45+
switch (call.method) {
46+
case 'paymentMethodMessagingElementDidUpdateHeight':
47+
final args = Map<String, dynamic>.from(call.arguments);
48+
final height = (args['height'] as num).toDouble();
49+
if (mounted) {
50+
setState(() {
51+
_height = height;
52+
});
53+
}
54+
widget.onHeightChange?.call(height);
55+
break;
56+
case 'paymentMethodMessagingElementConfigureResult':
57+
// The native element reports its configure status (loading / loaded /
58+
// no_content / failed). Not surfaced to Dart yet; handled here so the
59+
// channel doesn't log a MissingPluginException.
60+
break;
5461
}
5562
});
5663
}
@@ -61,7 +68,7 @@ class _PaymentMethodMessagingState extends State<PaymentMethodMessaging> {
6168
if (widget.configuration != oldWidget.configuration) {
6269
_methodChannel?.invokeMethod(
6370
'updateConfiguration',
64-
widget.configuration.toJson(),
71+
{'configuration': widget.configuration.toJson()},
6572
);
6673
}
6774
}
@@ -74,7 +81,7 @@ class _PaymentMethodMessagingState extends State<PaymentMethodMessaging> {
7481

7582
@override
7683
Widget build(BuildContext context) {
77-
final creationParams = widget.configuration.toJson();
84+
final creationParams = {'configuration': widget.configuration.toJson()};
7885

7986
Widget platform;
8087
if (defaultTargetPlatform == TargetPlatform.iOS) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3+
*
4+
* Do not edit this file as changes may cause incorrect behavior and will be lost
5+
* once the code is regenerated.
6+
*
7+
* @generated by codegen project: GeneratePropsJavaDelegate.js
8+
*/
9+
10+
package com.facebook.react.viewmanagers;
11+
12+
import android.view.View;
13+
import androidx.annotation.Nullable;
14+
import com.facebook.react.bridge.DynamicFromObject;
15+
import com.facebook.react.uimanager.BaseViewManager;
16+
import com.facebook.react.uimanager.BaseViewManagerDelegate;
17+
import com.facebook.react.uimanager.LayoutShadowNode;
18+
19+
public class PaymentMethodMessagingElementViewManagerDelegate<T extends View, U extends BaseViewManager<T, ? extends LayoutShadowNode> & PaymentMethodMessagingElementViewManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
20+
public PaymentMethodMessagingElementViewManagerDelegate(U viewManager) {
21+
super(viewManager);
22+
}
23+
@Override
24+
public void setProperty(T view, String propName, @Nullable Object value) {
25+
switch (propName) {
26+
case "appearance":
27+
mViewManager.setAppearance(view, new DynamicFromObject(value));
28+
break;
29+
case "configuration":
30+
mViewManager.setConfiguration(view, new DynamicFromObject(value));
31+
break;
32+
default:
33+
super.setProperty(view, propName, value);
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3+
*
4+
* Do not edit this file as changes may cause incorrect behavior and will be lost
5+
* once the code is regenerated.
6+
*
7+
* @generated by codegen project: GeneratePropsJavaInterface.js
8+
*/
9+
10+
package com.facebook.react.viewmanagers;
11+
12+
import android.view.View;
13+
import com.facebook.react.bridge.Dynamic;
14+
15+
public interface PaymentMethodMessagingElementViewManagerInterface<T extends View> {
16+
void setAppearance(T view, Dynamic value);
17+
void setConfiguration(T view, Dynamic value);
18+
}

packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
6161
EmbeddedPaymentElementViewManager()
6262
}
6363

64+
private val paymentMethodMessagingManager: PaymentMethodMessagingElementViewManager by lazy {
65+
PaymentMethodMessagingElementViewManager()
66+
}
67+
6468
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
6569
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(flutterPluginBinding.applicationContext)
6670

@@ -85,7 +89,7 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
8589
flutterPluginBinding
8690
.platformViewRegistry
8791
.registerViewFactory("flutter.stripe/embedded_payment_element", StripeSdkEmbeddedPaymentElementPlatformViewFactory(flutterPluginBinding, embeddedPaymentElementViewManager){stripeSdk})
88-
flutterPluginBinding.platformViewRegistry.registerViewFactory("flutter.stripe/payment_method_messaging", StripePaymentMethodMessagingPlatformViewFactory(flutterPluginBinding))
92+
flutterPluginBinding.platformViewRegistry.registerViewFactory("flutter.stripe/payment_method_messaging", StripePaymentMethodMessagingPlatformViewFactory(flutterPluginBinding, paymentMethodMessagingManager) { stripeSdk })
8993
}
9094

9195
override fun onMethodCall(call: MethodCall, result: Result) {
Lines changed: 67 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,104 @@
11
package com.flutter.stripe
22

3-
import android.app.Activity
4-
import android.app.Application
53
import android.content.Context
6-
import android.content.ContextWrapper
74
import android.view.View
8-
import android.widget.FrameLayout
9-
import androidx.compose.foundation.layout.Box
10-
import androidx.compose.runtime.getValue
11-
import androidx.compose.runtime.mutableStateOf
12-
import androidx.compose.runtime.remember
13-
import androidx.compose.runtime.setValue
14-
import androidx.compose.ui.Modifier
15-
import androidx.compose.ui.layout.onSizeChanged
16-
import androidx.compose.ui.platform.ComposeView
17-
import androidx.compose.ui.platform.LocalDensity
18-
import androidx.compose.ui.platform.ViewCompositionStrategy
19-
import com.stripe.android.paymentmethodmessaging.element.PaymentMethodMessagingElement
20-
import com.stripe.android.paymentmethodmessaging.element.PaymentMethodMessagingElementPreview
21-
import com.stripe.android.model.PaymentMethod
5+
import com.facebook.react.bridge.DynamicFromObject
6+
import com.facebook.react.bridge.ReadableMap
7+
import com.facebook.react.modules.core.DeviceEventManagerModule
8+
import com.facebook.react.uimanager.ThemedReactContext
9+
import com.reactnativestripesdk.PaymentMethodMessagingElementView
10+
import com.reactnativestripesdk.PaymentMethodMessagingElementViewManager
11+
import com.reactnativestripesdk.StripeSdkModule
2212
import io.flutter.plugin.common.MethodCall
2313
import io.flutter.plugin.common.MethodChannel
2414
import io.flutter.plugin.platform.PlatformView
25-
import kotlinx.coroutines.CoroutineScope
26-
import kotlinx.coroutines.Dispatchers
27-
import kotlinx.coroutines.Job
28-
import kotlinx.coroutines.SupervisorJob
29-
import kotlinx.coroutines.cancel
30-
import kotlinx.coroutines.launch
3115

32-
@OptIn(PaymentMethodMessagingElementPreview::class)
16+
/**
17+
* Thin wrapper around the Stripe SDK's React Native messaging view + its
18+
* [PaymentMethodMessagingElementViewManager] (vendored from stripe-react-native).
19+
*
20+
* Props (`configuration`, optional `appearance`) are pushed through the view manager,
21+
* exactly like the other native-SDK-backed views (see [StripeAubecsDebitPlatformView]).
22+
* The RN view reports its height and configure result through the shared module event
23+
* emitter; we register this view's per-view channel for the `paymentMethodMessagingElement`
24+
* event prefix so those events reach the matching Dart widget — the same mechanism
25+
* [StripeSdkEmbeddedPaymentElementPlatformView] uses.
26+
*
27+
* Nested creation params (the `paymentMethodTypes` array inside `configuration`) are
28+
* converted via [asReadableMap] rather than the flat `convertToReadables()` helper, so the
29+
* backing JSONObject auto-wraps inner lists/maps correctly.
30+
*/
3331
class StripePaymentMethodMessagingPlatformView(
34-
private val context: Context,
32+
context: Context,
3533
private val channel: MethodChannel,
34+
id: Int,
3635
creationParams: Map<String?, Any?>?,
37-
) : PlatformView {
36+
private val viewManager: PaymentMethodMessagingElementViewManager,
37+
sdkAccessor: () -> StripeSdkModule,
38+
) : PlatformView, MethodChannel.MethodCallHandler {
3839

39-
private val container: FrameLayout = FrameLayout(context)
40-
private val composeView: ComposeView = ComposeView(context).apply {
41-
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
42-
}
43-
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
44-
private var configureJob: Job? = null
45-
private var elementState: ((PaymentMethodMessagingElement?) -> Unit)? = null
46-
private var lastReportedHeightPx: Int = -1
40+
private val messagingView: PaymentMethodMessagingElementView =
41+
viewManager.createViewInstance(
42+
ThemedReactContext(sdkAccessor().reactContext, channel, sdkAccessor),
43+
)
4744

4845
init {
49-
container.addView(
50-
composeView,
51-
FrameLayout.LayoutParams(
52-
FrameLayout.LayoutParams.MATCH_PARENT,
53-
FrameLayout.LayoutParams.WRAP_CONTENT,
54-
),
55-
)
56-
composeView.setContent {
57-
var element by remember { mutableStateOf<PaymentMethodMessagingElement?>(null) }
58-
elementState = { element = it }
59-
val density = LocalDensity.current
60-
Box(
61-
modifier = Modifier.onSizeChanged { size ->
62-
if (size.height != lastReportedHeightPx) {
63-
lastReportedHeightPx = size.height
64-
val heightDp = with(density) { size.height.toDp().value }
65-
channel.invokeMethod(
66-
"onHeightChange",
67-
mapOf("height" to heightDp.toDouble()),
68-
)
69-
}
70-
},
71-
) {
72-
element?.Content(PaymentMethodMessagingElement.Appearance())
73-
}
74-
}
46+
channel.setMethodCallHandler(this)
47+
DeviceEventManagerModule.RCTDeviceEventEmitter.registerChannelForPrefix(EVENT_PREFIX, channel)
48+
applyProps(creationParams)
49+
}
7550

76-
channel.setMethodCallHandler { call, result -> handleMethodCall(call, result) }
77-
applyConfiguration(creationParams)
51+
override fun getView(): View = messagingView
52+
53+
override fun onFlutterViewAttached(flutterView: View) {
54+
viewManager.onAfterUpdateTransaction(messagingView)
7855
}
7956

80-
private fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) {
57+
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
8158
when (call.method) {
8259
"updateConfiguration" -> {
8360
@Suppress("UNCHECKED_CAST")
84-
applyConfiguration(call.arguments as? Map<String?, Any?>)
61+
applyProps(call.arguments as? Map<String?, Any?>)
8562
result.success(null)
8663
}
8764
else -> result.notImplemented()
8865
}
8966
}
9067

91-
private fun applyConfiguration(params: Map<String?, Any?>?) {
92-
configureJob?.cancel()
93-
elementState?.invoke(null)
94-
emitCollapsedHeight()
95-
96-
if (params == null) return
97-
98-
@Suppress("UNCHECKED_CAST")
99-
val methodStrings = params["paymentMethods"] as? List<String> ?: return
100-
val currency = params["currency"] as? String ?: return
101-
val amount = (params["amount"] as? Number)?.toLong() ?: return
102-
val countryCode = params["countryCode"] as? String
103-
val locale = params["locale"] as? String
104-
105-
val types = methodStrings.mapNotNull { PaymentMethod.Type.fromCode(it) }
106-
val application = context.findApplication() ?: return
68+
override fun dispose() {
69+
DeviceEventManagerModule.RCTDeviceEventEmitter.unregisterChannelForPrefix(EVENT_PREFIX)
70+
viewManager.onDropViewInstance(messagingView)
71+
channel.setMethodCallHandler(null)
72+
}
10773

108-
val configuration = PaymentMethodMessagingElement.Configuration().apply {
109-
amount(amount)
110-
currency(currency)
111-
locale?.let { locale(it) }
112-
countryCode?.let { countryCode(it) }
113-
if (types.isNotEmpty()) paymentMethodTypes(types)
74+
private fun applyProps(params: Map<String?, Any?>?) {
75+
asReadableMap(params?.get("appearance"))?.let {
76+
viewManager.setAppearance(messagingView, DynamicFromObject(it))
11477
}
115-
116-
configureJob = scope.launch {
117-
val element = PaymentMethodMessagingElement.create(application)
118-
val result = element.configure(configuration)
119-
when (result) {
120-
is PaymentMethodMessagingElement.ConfigureResult.Succeeded -> {
121-
elementState?.invoke(element)
122-
}
123-
is PaymentMethodMessagingElement.ConfigureResult.NoContent,
124-
is PaymentMethodMessagingElement.ConfigureResult.Failed -> {
125-
elementState?.invoke(null)
126-
emitCollapsedHeight()
127-
}
128-
}
78+
asReadableMap(params?.get("configuration"))?.let {
79+
viewManager.setConfiguration(messagingView, DynamicFromObject(it))
12980
}
13081
}
13182

132-
private fun emitCollapsedHeight() {
133-
if (lastReportedHeightPx != 0) {
134-
lastReportedHeightPx = 0
135-
channel.invokeMethod("onHeightChange", mapOf("height" to 0.0))
136-
}
83+
private fun asReadableMap(value: Any?): ReadableMap? {
84+
if (value !is Map<*, *>) return null
85+
@Suppress("UNCHECKED_CAST")
86+
return ReadableMap(normalizeMap(value) as Map<String, Any>)
13787
}
13888

139-
override fun getView(): View = container
89+
private fun normalizeMap(value: Map<*, *>): Map<String, Any?> =
90+
value.entries
91+
.filter { it.key is String }
92+
.associate { (key, entryValue) -> key as String to normalizeValue(entryValue) }
14093

141-
override fun dispose() {
142-
configureJob?.cancel()
143-
scope.cancel()
144-
composeView.disposeComposition()
145-
channel.setMethodCallHandler(null)
146-
}
147-
}
94+
private fun normalizeValue(value: Any?): Any? =
95+
when (value) {
96+
is Map<*, *> -> normalizeMap(value)
97+
is List<*> -> value.map { normalizeValue(it) }
98+
else -> value
99+
}
148100

149-
private fun Context.findApplication(): Application? {
150-
var ctx: Context = this
151-
while (ctx is ContextWrapper) {
152-
if (ctx is Application) return ctx
153-
if (ctx is Activity) return ctx.application
154-
ctx = ctx.baseContext
101+
private companion object {
102+
const val EVENT_PREFIX = "paymentMethodMessagingElement"
155103
}
156-
return applicationContext as? Application
157104
}

packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripePaymentMethodMessagingPlatformViewFactory.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.flutter.stripe
22

33
import android.content.Context
4+
import com.reactnativestripesdk.PaymentMethodMessagingElementViewManager
5+
import com.reactnativestripesdk.StripeSdkModule
46
import io.flutter.embedding.engine.plugins.FlutterPlugin
57
import io.flutter.plugin.common.MethodChannel
68
import io.flutter.plugin.common.StandardMessageCodec
@@ -9,6 +11,8 @@ import io.flutter.plugin.platform.PlatformViewFactory
911

1012
class StripePaymentMethodMessagingPlatformViewFactory(
1113
private val flutterPluginBinding: FlutterPlugin.FlutterPluginBinding,
14+
private val viewManager: PaymentMethodMessagingElementViewManager,
15+
private val sdkAccessor: () -> StripeSdkModule,
1216
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
1317
override fun create(context: Context?, viewId: Int, args: Any?): PlatformView {
1418
if (context == null) {
@@ -20,6 +24,13 @@ class StripePaymentMethodMessagingPlatformViewFactory(
2024
)
2125
@Suppress("UNCHECKED_CAST")
2226
val creationParams = args as? Map<String?, Any?>?
23-
return StripePaymentMethodMessagingPlatformView(context, channel, creationParams)
27+
return StripePaymentMethodMessagingPlatformView(
28+
context,
29+
channel,
30+
viewId,
31+
creationParams,
32+
viewManager,
33+
sdkAccessor,
34+
)
2435
}
2536
}

0 commit comments

Comments
 (0)