From ae51739acb9704c5e51da853458942a895924f22 Mon Sep 17 00:00:00 2001 From: jonasbark Date: Wed, 21 Jul 2021 14:13:50 +0200 Subject: [PATCH] Sync (#217) * sync android code from react native * BREAKING: rename confirmpaymentmethod to confirmpayment * update token result object * add methods blur, focus and clear to cardfield * make sure CardEditEvent is not exported by extracting it to a different file * Update iOS side * Display error also in debug * Update focus and blur methods on iOS Co-authored-by: Remon Co-authored-by: Jaime Blasco --- example/lib/screens/google_pay_screen.dart | 2 +- .../screens/no_webhook_payment_screen.dart | 161 +++++++++++------- .../screens/setup_future_payment_screen.dart | 2 +- .../lib/screens/webhook_payment_screen.dart | 2 +- example/lib/widgets/loading_button.dart | 9 +- packages/stripe/lib/flutter_stripe.dart | 1 + packages/stripe/lib/src/stripe.dart | 12 +- .../lib/src/widgets/card_edit_controller.dart | 22 +++ .../lib/src/widgets/card_edit_event.dart | 1 + .../stripe/lib/src/widgets/card_field.dart | 96 ++++++++--- .../react/uimanager/SimpleViewManager.kt | 6 +- .../com/flutter/stripe/StripeAndroidPlugin.kt | 2 +- .../stripe/StripeSdkCardPlatformView.kt | 1 + .../com/reactnativestripesdk/Constants.kt | 1 + .../com/reactnativestripesdk/Mappers.kt | 2 +- .../PaymentMethodCreateParamsFactory.kt | 1 + .../PaymentSheetFragment.kt | 12 +- .../reactnativestripesdk/StripeSdkCardView.kt | 31 ++++ .../StripeSdkCardViewManager.kt | 11 +- .../reactnativestripesdk/StripeSdkModule.kt | 39 +++-- .../ios/Classes/CardFieldView.swift | 17 +- packages/stripe_ios/ios/Classes/Mappers.swift | 3 +- .../stripe_ios/ios/Classes/StripePlugin.swift | 2 +- .../stripe_ios/ios/Classes/StripeSdk.swift | 19 ++- .../lib/src/method_channel_stripe.dart | 21 +-- .../lib/src/stripe_platform_interface.dart | 2 +- .../test/method_channel_stripe_test.dart | 18 +- .../test/test_data.dart | 26 +-- 28 files changed, 345 insertions(+), 177 deletions(-) create mode 100644 packages/stripe/lib/src/widgets/card_edit_controller.dart create mode 100644 packages/stripe/lib/src/widgets/card_edit_event.dart diff --git a/example/lib/screens/google_pay_screen.dart b/example/lib/screens/google_pay_screen.dart index 1edac958b..345efcb0c 100644 --- a/example/lib/screens/google_pay_screen.dart +++ b/example/lib/screens/google_pay_screen.dart @@ -91,7 +91,7 @@ class _GooglePayScreenState extends State { ); // 3. Confirm Google pay payment method - await Stripe.instance.confirmPaymentMethod( + await Stripe.instance.confirmPayment( clientSecret, params, ); diff --git a/example/lib/screens/no_webhook_payment_screen.dart b/example/lib/screens/no_webhook_payment_screen.dart index 256e007b0..ada02724a 100644 --- a/example/lib/screens/no_webhook_payment_screen.dart +++ b/example/lib/screens/no_webhook_payment_screen.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:http/http.dart' as http; import 'package:stripe_example/widgets/loading_button.dart'; import 'package:stripe_platform_interface/stripe_platform_interface.dart'; @@ -15,6 +15,13 @@ class NoWebhookPaymentScreen extends StatefulWidget { class _NoWebhookPaymentScreenState extends State { CardFieldInputDetails? _card; + final _editController = CardEditController(); + + @override + void dispose() { + _editController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -25,6 +32,7 @@ class _NoWebhookPaymentScreenState extends State { Padding( padding: EdgeInsets.all(16), child: CardField( + controller: _editController, onCardChanged: (card) { setState(() { _card = card; @@ -39,6 +47,32 @@ class _NoWebhookPaymentScreenState extends State { text: 'Pay', ), ), + Divider( + thickness: 2, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: ElevatedButton( + onPressed: () => _editController.blur(), + child: Text('Blur'), + ), + ), + ElevatedButton( + onPressed: () => _editController.clear(), + child: Text('Clear'), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: ElevatedButton( + onPressed: () => _editController.focus(), + child: Text('Focus'), + ), + ), + ], + ), ], ), ); @@ -50,82 +84,81 @@ class _NoWebhookPaymentScreenState extends State { } try { + // 1. Gather customer billing information (ex. email) + + final billingDetails = BillingDetails( + email: 'email@stripe.com', + phone: '+48888000888', + address: Address( + city: 'Houston', + country: 'US', + line1: '1459 Circle Drive', + line2: '', + state: 'Texas', + postalCode: '77063', + ), + ); // mocked data for tests + + // 2. Create payment method + final paymentMethod = + await Stripe.instance.createPaymentMethod(PaymentMethodParams.card( + billingDetails: billingDetails, + )); + + // 3. call API to create PaymentIntent + final paymentIntentResult = await callNoWebhookPayEndpointMethodId( + useStripeSdk: true, + paymentMethodId: paymentMethod.id, + currency: 'usd', // mocked data + items: [ + {'id': 'id'} + ], + ); - // 1. Gather customer billing information (ex. email) - - final billingDetails = BillingDetails( - email: 'email@stripe.com', - phone: '+48888000888', - address: Address( - city: 'Houston', - country: 'US', - line1: '1459 Circle Drive', - line2: '', - state: 'Texas', - postalCode: '77063', - ), - ); // mocked data for tests - - // 2. Create payment method - final paymentMethod = - await Stripe.instance.createPaymentMethod(PaymentMethodParams.card( - billingDetails: billingDetails, - )); - - // 3. call API to create PaymentIntent - final paymentIntentResult = await callNoWebhookPayEndpointMethodId( - useStripeSdk: true, - paymentMethodId: paymentMethod.id, - currency: 'usd', // mocked data - items: [ - {'id': 'id'} - ], - ); - - if (paymentIntentResult['error'] != null) { - // Error during creating or confirming Intent - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: ${paymentIntentResult['error']}'))); - return; - } + if (paymentIntentResult['error'] != null) { + // Error during creating or confirming Intent + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${paymentIntentResult['error']}'))); + return; + } - if (paymentIntentResult['clientSecret'] != null && - paymentIntentResult['requiresAction'] == null) { - // Payment succedeed + if (paymentIntentResult['clientSecret'] != null && + paymentIntentResult['requiresAction'] == null) { + // Payment succedeed - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Success!: The payment was confirmed successfully!'))); - return; - } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: + Text('Success!: The payment was confirmed successfully!'))); + return; + } - if (paymentIntentResult['clientSecret'] != null && - paymentIntentResult['requiresAction'] == true) { - // 4. if payment requires action calling handleCardAction - final paymentIntent = await Stripe.instance - .handleCardAction(paymentIntentResult['clientSecret']); + if (paymentIntentResult['clientSecret'] != null && + paymentIntentResult['requiresAction'] == true) { + // 4. if payment requires action calling handleCardAction + final paymentIntent = await Stripe.instance + .handleCardAction(paymentIntentResult['clientSecret']); - // todo handle error - /*if (cardActionError) { + // todo handle error + /*if (cardActionError) { Alert.alert( `Error code: ${cardActionError.code}`, cardActionError.message ); } else*/ - if (paymentIntent.status == PaymentIntentsStatus.RequiresConfirmation) { - // 5. Call API to confirm intent - await confirmIntent(paymentIntent.id); - } else { - // Payment succedeed - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: ${paymentIntentResult['error']}'))); + if (paymentIntent.status == PaymentIntentsStatus.RequiresConfirmation) { + // 5. Call API to confirm intent + await confirmIntent(paymentIntent.id); + } else { + // Payment succedeed + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Error: ${paymentIntentResult['error']}'))); + } } - } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e'))); - rethrow; + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Error: $e'))); + rethrow; } } diff --git a/example/lib/screens/setup_future_payment_screen.dart b/example/lib/screens/setup_future_payment_screen.dart index 7136af86e..9fe9d2bb1 100644 --- a/example/lib/screens/setup_future_payment_screen.dart +++ b/example/lib/screens/setup_future_payment_screen.dart @@ -186,7 +186,7 @@ class _SetupFuturePaymentScreenState extends State { // TODO lastPaymentError if (_retrievedPaymentIntent?.paymentMethodId != null && _card != null) { - await Stripe.instance.confirmPaymentMethod( + await Stripe.instance.confirmPayment( _retrievedPaymentIntent!.clientSecret, PaymentMethodParams.cardFromMethodId( paymentMethodId: _retrievedPaymentIntent!.paymentMethodId!), diff --git a/example/lib/screens/webhook_payment_screen.dart b/example/lib/screens/webhook_payment_screen.dart index 69be13792..406bc13f1 100644 --- a/example/lib/screens/webhook_payment_screen.dart +++ b/example/lib/screens/webhook_payment_screen.dart @@ -96,7 +96,7 @@ class _WebhookPaymentScreenState extends State { // 3. Confirm payment with card details // The rest will be done automatically using webhooks // ignore: unused_local_variable - final paymentIntent = await Stripe.instance.confirmPaymentMethod( + final paymentIntent = await Stripe.instance.confirmPayment( clientSecret['clientSecret'], PaymentMethodParams.card( billingDetails: billingDetails, diff --git a/example/lib/widgets/loading_button.dart b/example/lib/widgets/loading_button.dart index cc8299374..0028e4b00 100644 --- a/example/lib/widgets/loading_button.dart +++ b/example/lib/widgets/loading_button.dart @@ -29,22 +29,17 @@ class _LoadingButtonState extends State { setState(() { _isLoading = true; }); - + try { await widget.onPressed!(); } catch (e) { - if (kDebugMode) { - rethrow; - } else { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Error $e'))); - } + rethrow; } finally { setState(() { _isLoading = false; }); } - - } } diff --git a/packages/stripe/lib/flutter_stripe.dart b/packages/stripe/lib/flutter_stripe.dart index cb3084441..bd46f533a 100644 --- a/packages/stripe/lib/flutter_stripe.dart +++ b/packages/stripe/lib/flutter_stripe.dart @@ -4,3 +4,4 @@ export 'src/model/apple_pay_button.dart'; export 'src/stripe.dart'; export 'src/widgets/apple_pay_button.dart'; export 'src/widgets/card_field.dart'; +export 'src/widgets/card_edit_controller.dart'; diff --git a/packages/stripe/lib/src/stripe.dart b/packages/stripe/lib/src/stripe.dart index 559509f68..0edab68d6 100644 --- a/packages/stripe/lib/src/stripe.dart +++ b/packages/stripe/lib/src/stripe.dart @@ -155,7 +155,7 @@ class Stripe { /// Retrieves a [PaymentIntent] using the provided [clientSecret]. /// - /// Throws a [StripeException] in case retrieving the intent fails. + /// Throws a [StripeException] in case retrieving the intent fails. Future retrievePaymentIntent(String clientSecret) async { await _awaitForSettings(); try { @@ -207,16 +207,18 @@ class Stripe { /// Confirms a payment method, using the provided [paymentIntentClientSecret] /// and [data]. /// - /// See [PaymentMethodParams] for more details. - /// Throws a [StripeException] when confirming the paymentmethod fails. - Future confirmPaymentMethod( + /// See [PaymentMethodParams] for more details. The method returns a + /// [PaymentIntent]. Throws a [StripeException] when confirming the + /// paymentmethod fails. + + Future confirmPayment( String paymentIntentClientSecret, PaymentMethodParams data, [ Map options = const {}, ]) async { await _awaitForSettings(); try { - final paymentMethod = await _platform.confirmPaymentMethod( + final paymentMethod = await _platform.confirmPayment( paymentIntentClientSecret, data, options); return paymentMethod; } on StripeError { diff --git a/packages/stripe/lib/src/widgets/card_edit_controller.dart b/packages/stripe/lib/src/widgets/card_edit_controller.dart new file mode 100644 index 000000000..414d289e2 --- /dev/null +++ b/packages/stripe/lib/src/widgets/card_edit_controller.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'card_edit_event.dart'; + +class CardEditController extends ValueNotifier { + CardEditController() : super(CardEditEvent.none); + + void focus() { + value = CardEditEvent.focus; + notifyListeners(); + } + + void blur() { + value = CardEditEvent.blur; + notifyListeners(); + } + + void clear() { + value = CardEditEvent.clear; + notifyListeners(); + } +} diff --git a/packages/stripe/lib/src/widgets/card_edit_event.dart b/packages/stripe/lib/src/widgets/card_edit_event.dart new file mode 100644 index 000000000..872e04892 --- /dev/null +++ b/packages/stripe/lib/src/widgets/card_edit_event.dart @@ -0,0 +1 @@ +enum CardEditEvent { none, focus, blur, clear } diff --git a/packages/stripe/lib/src/widgets/card_field.dart b/packages/stripe/lib/src/widgets/card_field.dart index 0b8756b39..90e355bf1 100644 --- a/packages/stripe/lib/src/widgets/card_field.dart +++ b/packages/stripe/lib/src/widgets/card_field.dart @@ -8,23 +8,27 @@ import 'package:flutter/services.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:stripe_platform_interface/stripe_platform_interface.dart'; +import 'card_edit_controller.dart'; +import 'card_edit_event.dart'; + /// Customizable form that collects card information. class CardField extends StatefulWidget { - const CardField({ - required this.onCardChanged, - Key? key, - this.onFocus, - this.decoration, - this.enablePostalCode = false, - this.style, - this.autofocus = false, - this.dangerouslyGetFullCardDetails = false, - this.cursorColor, - this.numberHintText, - this.expirationHintText, - this.cvcHintText, - this.postalCodeHintText, - }) : super(key: key); + const CardField( + {required this.onCardChanged, + Key? key, + this.onFocus, + this.decoration, + this.enablePostalCode = false, + this.style, + this.autofocus = false, + this.dangerouslyGetFullCardDetails = false, + this.cursorColor, + this.numberHintText, + this.expirationHintText, + this.cvcHintText, + this.postalCodeHintText, + this.controller}) + : super(key: key); /// Decoration related to the input fields. final InputDecoration? decoration; @@ -61,6 +65,10 @@ class CardField extends StatefulWidget { /// Default is `false`. final bool autofocus; + /// Controller that can be use to execute several operations on the cardfield + /// e.g (clear). + final CardEditController? controller; + /// When true the Full card details will be returned. /// /// WARNING!!! Only do this if you're certain that you fulfill the necessary @@ -78,9 +86,12 @@ class _CardFieldState extends State { final FocusNode _node = FocusNode(debugLabel: 'CardField', descendantsAreFocusable: false); + late CardEditController controller; + @override void initState() { _node.addListener(updateState); + controller = widget.controller ?? CardEditController(); super.initState(); } @@ -89,6 +100,8 @@ class _CardFieldState extends State { _node ..removeListener(updateState) ..dispose(); + + controller.dispose(); super.dispose(); } @@ -119,6 +132,7 @@ class _CardFieldState extends State { child: _MethodChannelCardField( height: platformCardHeight, focusNode: _node, + controller: controller, style: style, placeholder: CardPlaceholder( number: widget.numberHintText, @@ -192,6 +206,7 @@ class _NegativeMarginLayout extends SingleChildLayoutDelegate { class _MethodChannelCardField extends StatefulWidget { _MethodChannelCardField({ required this.onCardChanged, + required this.controller, Key? key, this.onFocus, this.style, @@ -217,6 +232,7 @@ class _MethodChannelCardField extends StatefulWidget { final bool enablePostalCode; final FocusNode? focusNode; final bool autofocus; + final CardEditController controller; // This is used in the platform side to register the view. static const _viewType = 'flutter.stripe/card_field'; @@ -268,6 +284,23 @@ class _MethodChannelCardFieldState extends State<_MethodChannelCardField> { cvc: 'CVC', ).apply(placeholder); + @override + void initState() { + widget.controller.addListener(() { + _handleCardEditEvent(widget.controller.value); + }); + super.initState(); + } + + @override + void dispose() { + widget.controller.removeListener(() { + _handleCardEditEvent(widget.controller.value); + }); + _focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final style = resolveStyle(widget.style); @@ -399,6 +432,29 @@ class _MethodChannelCardFieldState extends State<_MethodChannelCardField> { } } + /// handle event that is emitted from the editting controller and propagate it + /// through native + /// + void _handleCardEditEvent(CardEditEvent event) { + if (_methodChannel == null) { + return; + } else { + switch (event) { + case CardEditEvent.none: + break; + case CardEditEvent.focus: + _methodChannel!.invokeMethod('focus'); + break; + case CardEditEvent.blur: + _methodChannel!.invokeMethod('blur'); + break; + case CardEditEvent.clear: + _methodChannel!.invokeMethod('clear'); + break; + } + } + } + /// Handler called when the focus changes in the node attached to the platform /// view. This updates the correspondant platform view to keep it in sync. void _handleFrameworkFocusChanged(bool isFocused) { @@ -408,17 +464,11 @@ class _MethodChannelCardFieldState extends State<_MethodChannelCardField> { } setState(() {}); if (!isFocused) { - methodChannel.invokeMethod('clearFocus'); + methodChannel.invokeMethod('blur'); return; } - methodChannel.invokeMethod('requestFocus'); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); + methodChannel.invokeMethod('focus'); } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/SimpleViewManager.kt b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/SimpleViewManager.kt index faa430ee9..48629eb11 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/SimpleViewManager.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/SimpleViewManager.kt @@ -1,7 +1,7 @@ package com.facebook.react.uimanager import android.view.View -import com.reactnativestripesdk.AuBECSDebitFormView +import com.facebook.react.bridge.ReadableArray abstract class SimpleViewManager { @@ -11,7 +11,7 @@ abstract class SimpleViewManager { abstract fun createViewInstance(reactContext: ThemedReactContext): T - open fun onDropViewInstance(view: T) { + open fun onDropViewInstance(view: T) {} - } + open fun receiveCommand(root: T, commandId: String?, args: ReadableArray?) {} } diff --git a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt index fff22ef60..f10b68504 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt @@ -74,7 +74,7 @@ class StripeAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { paymentIntentClientSecret = call.requiredArgument("paymentIntentClientSecret"), promise = Promise(result) ) - "confirmPaymentMethod" -> stripeSdk.confirmPaymentMethod( + "confirmPayment" -> stripeSdk.confirmPayment( paymentIntentClientSecret = call.requiredArgument("paymentIntentClientSecret"), params = call.requiredArgument("params"), options = call.requiredArgument("options"), diff --git a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkCardPlatformView.kt b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkCardPlatformView.kt index 1a428c4e5..e0e4c2b87 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkCardPlatformView.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeSdkCardPlatformView.kt @@ -124,6 +124,7 @@ class StripeSdkCardPlatformView( cardView.clearFocus() result.success(null) } + "focus", "blur", "clear" -> stripeSdkCardViewManager.receiveCommand(cardView, call.method, null) } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Constants.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Constants.kt index c3ec00713..94791591c 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Constants.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Constants.kt @@ -3,4 +3,5 @@ package com.reactnativestripesdk var ON_PAYMENT_RESULT_ACTION = "com.reactnativestripesdk.PAYMENT_RESULT_ACTION" var ON_PAYMENT_OPTION_ACTION = "com.reactnativestripesdk.PAYMENT_OPTION_ACTION" var ON_CONFIGURE_FLOW_CONTROLLER = "com.reactnativestripesdk.CONFIGURE_FLOW_CONTROLLER_ACTION" +var ON_INIT_PAYMENT_SHEET = "com.reactnativestripesdk.INIT_PAYMENT_SHEET" var ON_FRAGMENT_CREATED = "com.reactnativestripesdk.FRAGMENT_CREATED_ACTION" diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt index 396f98e2d..c729aa2a0 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt @@ -464,7 +464,7 @@ fun getBooleanOrFalse(map: ReadableMap?, key: String): Boolean { } private fun convertToUnixTimestamp(timestamp: Long): String { - return (timestamp * 1000).toInt().toString() + return (timestamp * 1000).toString() } fun mapToUICustomization(params: ReadableMap): PaymentAuthConfig.Stripe3ds2UiCustomization { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt index dcef63055..917320c08 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt @@ -105,6 +105,7 @@ class PaymentMethodCreateParamsFactory(private val clientSecret: String, private paymentMethodId = paymentMethodId, paymentMethodOptions = paymentMethodOptionParams, clientSecret = clientSecret, + setupFutureUsage = setupFutureUsage, returnUrl = mapToReturnURL(urlScheme) ) } else { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentSheetFragment.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentSheetFragment.kt index 08d99e050..a49f30523 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentSheetFragment.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentSheetFragment.kt @@ -15,7 +15,10 @@ import android.widget.FrameLayout import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.DrawableCompat import androidx.fragment.app.Fragment -import com.stripe.android.paymentsheet.* +import com.stripe.android.paymentsheet.PaymentOptionCallback +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.PaymentSheetResult +import com.stripe.android.paymentsheet.PaymentSheetResultCallback import com.stripe.android.paymentsheet.model.PaymentOption import java.io.ByteArrayOutputStream @@ -42,6 +45,7 @@ class PaymentSheetFragment : Fragment() { val customerId = arguments?.getString("customerId").orEmpty() val customerEphemeralKeySecret = arguments?.getString("customerEphemeralKeySecret").orEmpty() val countryCode = arguments?.getString("countryCode").orEmpty() + val googlePayEnabled = arguments?.getBoolean("googlePay") val testEnv = arguments?.getBoolean("testEnv") paymentIntentClientSecret = arguments?.getString("paymentIntentClientSecret").orEmpty() setupIntentClientSecret = arguments?.getString("setupIntentClientSecret").orEmpty() @@ -76,10 +80,10 @@ class PaymentSheetFragment : Fragment() { id = customerId, ephemeralKeySecret = customerEphemeralKeySecret ) else null, - googlePay = PaymentSheet.GooglePayConfiguration( + googlePay = if (googlePayEnabled == true) PaymentSheet.GooglePayConfiguration( environment = if (testEnv == true) PaymentSheet.GooglePayConfiguration.Environment.Test else PaymentSheet.GooglePayConfiguration.Environment.Production, countryCode = countryCode - ) + ) else null ) if (arguments?.getBoolean("customFlow") == true) { @@ -87,6 +91,8 @@ class PaymentSheetFragment : Fragment() { configureFlowController() } else { paymentSheet = PaymentSheet(this, paymentResultCallback) + val intent = Intent(ON_INIT_PAYMENT_SHEET) + activity?.sendBroadcast(intent) } val intent = Intent(ON_FRAGMENT_CREATED) diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardView.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardView.kt index b346168f8..c05753b84 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardView.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardView.kt @@ -52,6 +52,28 @@ class StripeSdkCardView(private val context: ThemedReactContext) : FrameLayout(c } } + fun requestFocusFromJS() { + val binding = CardInputWidgetBinding.bind(mCardWidget) + binding.cardNumberEditText.requestFocus() + binding.cardNumberEditText.showSoftKeyboard() + } + + fun requestBlurFromJS() { + val binding = CardInputWidgetBinding.bind(mCardWidget) + binding.cardNumberEditText.hideSoftKeyboard() + binding.cardNumberEditText.clearFocus() + binding.container.requestFocus() + } + + fun requestClearFromJS() { + val binding = CardInputWidgetBinding.bind(mCardWidget) + binding.cardNumberEditText.setText("") + binding.cvcEditText.setText("") + binding.expiryDateEditText.setText("") + if (mCardWidget.postalCodeEnabled) { + binding.postalCodeEditText.setText("") + } + } fun setCardStyle(value: ReadableMap) { val binding = CardInputWidgetBinding.bind(mCardWidget) @@ -245,3 +267,12 @@ fun View.showSoftKeyboard() { } } } + +fun View.hideSoftKeyboard() { + post { + if (this.requestFocus()) { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(windowToken, 0) + } + } +} diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardViewManager.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardViewManager.kt index e0187312d..e838d56e5 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardViewManager.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkCardViewManager.kt @@ -1,6 +1,7 @@ package com.reactnativestripesdk -import com.facebook.react.bridge.* +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext @@ -19,6 +20,14 @@ class StripeSdkCardViewManager : SimpleViewManager() { CardChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCardChange")) } + override fun receiveCommand(root: StripeSdkCardView, commandId: String?, args: ReadableArray?) { + when (commandId) { + "focus" -> root.requestFocusFromJS() + "blur" -> root.requestBlurFromJS() + "clear" -> root.requestClearFromJS() + } + } + @ReactProp(name = "dangerouslyGetFullCardDetails") fun setDangerouslyGetFullCardDetails(view: StripeSdkCardView, dangerouslyGetFullCardDetails: Boolean = false) { view.setDangerouslyGetFullCardDetails(dangerouslyGetFullCardDetails); diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt index 6576adfd1..6ab0c608c 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt @@ -9,7 +9,6 @@ import android.os.AsyncTask import android.os.Bundle import android.os.Parcelable import android.util.Log -import androidx.appcompat.app.AppCompatActivity import com.facebook.react.bridge.* import com.stripe.android.* import com.stripe.android.model.* @@ -190,7 +189,9 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S presentPaymentSheetPromise?.resolve(WritableNativeMap()) } } - else if (intent.action == ON_CONFIGURE_FLOW_CONTROLLER) { + else if (intent.action == ON_INIT_PAYMENT_SHEET) { + initPaymentSheetPromise?.resolve(WritableNativeMap()) + } else if (intent.action == ON_CONFIGURE_FLOW_CONTROLLER) { val label = intent.extras?.getString("label") val image = intent.extras?.getString("image") @@ -235,6 +236,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S this.currentActivity?.registerReceiver(mPaymentSheetReceiver, IntentFilter(ON_PAYMENT_OPTION_ACTION)); this.currentActivity?.registerReceiver(mPaymentSheetReceiver, IntentFilter(ON_CONFIGURE_FLOW_CONTROLLER)); this.currentActivity?.registerReceiver(mPaymentSheetReceiver, IntentFilter(ON_FRAGMENT_CREATED)); + this.currentActivity?.registerReceiver(mPaymentSheetReceiver, IntentFilter(ON_INIT_PAYMENT_SHEET)); promise.resolve(null) } @@ -255,6 +257,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S val merchantDisplayName = getValOr(params, "merchantDisplayName") val countryCode = getValOr(params, "merchantCountryCode") val testEnv = getBooleanOrNull(params, "testEnv") ?: false + val googlePay = getBooleanOrNull(params, "googlePay") ?: false this.initPaymentSheetPromise = promise @@ -268,15 +271,13 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S bundle.putString("countryCode", countryCode) bundle.putBoolean("customFlow", customFlow) bundle.putBoolean("testEnv", testEnv) + bundle.putBoolean("googlePay", googlePay) it.arguments = bundle } activity.supportFragmentManager.beginTransaction() .add(fragment, "payment_sheet_launch_fragment") .commit() - if (!customFlow) { - this.initPaymentSheetPromise?.resolve(WritableNativeMap()) - } } @ReactMethod @@ -307,7 +308,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S private fun onFpxPaymentMethodResult(result: AddPaymentMethodActivityStarter.Result) { when (result) { is AddPaymentMethodActivityStarter.Result.Success -> { - stripe.confirmPayment(currentActivity!!.activity, + stripe.confirmPayment(currentActivity.activity, ConfirmPaymentIntentParams.createWithPaymentMethodId( result.paymentMethod.id!!, confirmPaymentClientSecret!!, @@ -351,14 +352,14 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S fun createToken(params: ReadableMap, promise: Promise) { val type = getValOr(params, "type", null)?.let { if (it != "Card") { - promise.reject(CreateTokenErrorType.Failed.toString(), "$it type is not supported yet") + promise.resolve(createError(CreateTokenErrorType.Failed.toString(), "$it type is not supported yet")) return } } val address = getMapOrNull(params, "address") val instance = cardFieldManager.getCardViewInstance() val cardParams = instance?.cardParams?.toParamMap() ?: run { - promise.reject(CreateTokenErrorType.Failed.toString(), "Card details not complete") + promise.resolve(createError(CreateTokenErrorType.Failed.toString(), "Card details not complete")) return } @@ -371,11 +372,15 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S name = getValOr(params, "name", null) ) runBlocking { - val token = stripe.createCardToken( - cardParams = params, - stripeAccountId = stripeAccountId - ) - promise.resolve(mapFromToken(token)) + try { + val token = stripe.createCardToken( + cardParams = params, + stripeAccountId = stripeAccountId + ) + promise.resolve(createResult("token", mapFromToken(token))) + } catch (e: Exception) { + promise.resolve(createError(CreateTokenErrorType.Failed.toString(), e.message)) + } } } @@ -400,15 +405,15 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S @ReactMethod fun handleCardAction(paymentIntentClientSecret: String, promise: Promise) { - val activity = currentActivity + val activity = currentActivity.activity if (activity != null) { handleCardActionPromise = promise - stripe.handleNextActionForPayment(activity.activity, paymentIntentClientSecret) + stripe.handleNextActionForPayment(activity, paymentIntentClientSecret) } } @ReactMethod - fun confirmPaymentMethod(paymentIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise) { + fun confirmPayment(paymentIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise) { confirmPromise = promise confirmPaymentClientSecret = paymentIntentClientSecret @@ -432,7 +437,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext, cardFieldManager: S try { val confirmParams = factory.createConfirmParams(paymentMethodType) confirmParams.shipping = mapToShippingDetails(getMapOrNull(params, "shippingDetails")) - stripe.confirmPayment(currentActivity!!.activity, confirmParams) + stripe.confirmPayment(currentActivity.activity, confirmParams) } catch (error: PaymentMethodCreateParamsException) { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), error)) } diff --git a/packages/stripe_ios/ios/Classes/CardFieldView.swift b/packages/stripe_ios/ios/Classes/CardFieldView.swift index 0c1e37788..80dbe667e 100644 --- a/packages/stripe_ios/ios/Classes/CardFieldView.swift +++ b/packages/stripe_ios/ios/Classes/CardFieldView.swift @@ -134,11 +134,14 @@ class CardFieldView: NSObject, FlutterPlatformView, STPPaymentCardTextFieldDeleg updatePlaceholder(arguments) } result(nil) - case "requestFocus": - focus() + case "focus": + focus() result(nil) - case "clearFocus": - clearFocus() + case "blur": + blur() + result(nil) + case "clear": + clear() result(nil) default: result(FlutterMethodNotImplemented) @@ -279,8 +282,12 @@ class CardFieldView: NSObject, FlutterPlatformView, STPPaymentCardTextFieldDeleg cardField.becomeFirstResponder() } - func clearFocus() { + func blur() { cardField.resignFirstResponder() } + func clear() { + cardField.clear() + } + } diff --git a/packages/stripe_ios/ios/Classes/Mappers.swift b/packages/stripe_ios/ios/Classes/Mappers.swift index 3b1cd41c9..6e890dd26 100644 --- a/packages/stripe_ios/ios/Classes/Mappers.swift +++ b/packages/stripe_ios/ios/Classes/Mappers.swift @@ -823,7 +823,8 @@ class Mappers { class func convertDateToUnixTimestamp(date: Date?) -> String? { if let date = date { - return String(date.timeIntervalSince1970 * 1000.0) + let value = date.timeIntervalSince1970 * 1000.0 + return String(format: "%.0f", value) } return nil } diff --git a/packages/stripe_ios/ios/Classes/StripePlugin.swift b/packages/stripe_ios/ios/Classes/StripePlugin.swift index ecb265041..f2dc450c5 100644 --- a/packages/stripe_ios/ios/Classes/StripePlugin.swift +++ b/packages/stripe_ios/ios/Classes/StripePlugin.swift @@ -63,7 +63,7 @@ public class StripePlugin: StripeSdk, FlutterPlugin { return configure3dSecure(call, result: result) case "handleCardAction": return handleCardAction(call, result: result) - case "confirmPaymentMethod": + case "confirmPayment": return confirmPayment(call, result: result) case "retrievePaymentIntent": return retrievePaymentIntent(call, result: result) diff --git a/packages/stripe_ios/ios/Classes/StripeSdk.swift b/packages/stripe_ios/ios/Classes/StripeSdk.swift index ec6460807..b5590306b 100644 --- a/packages/stripe_ios/ios/Classes/StripeSdk.swift +++ b/packages/stripe_ios/ios/Classes/StripeSdk.swift @@ -154,7 +154,9 @@ public class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSele DispatchQueue.main.async { if (confirmPayment == false) { - self.paymentSheetFlowController?.presentPaymentOptions(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) { + self.paymentSheetFlowController?.presentPaymentOptions(from: + findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + ) { if let paymentOption = self.paymentSheetFlowController?.paymentOption { let option: NSDictionary = [ "label": paymentOption.label, @@ -166,7 +168,9 @@ public class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSele } } } else { - self.paymentSheet?.present(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) { paymentResult in + self.paymentSheet?.present(from: + findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + ) { paymentResult in switch paymentResult { case .completed: resolve([]) @@ -509,7 +513,7 @@ public class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSele STPAPIClient.shared.createToken(withCard: cardSourceParams) { token, error in if let token = token { - resolve(Mappers.mapFromToken(token: token)) + resolve(Mappers.createResult("token", Mappers.mapFromToken(token: token))) } else { resolve(Errors.createError(CreateTokenErrorType.Failed.rawValue, error?.localizedDescription)) } @@ -592,6 +596,7 @@ public class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSele paymentMethodOptions = try factory.createOptions(paymentMethodType: paymentMethodType) } catch { resolve(Errors.createError(ConfirmPaymentErrorType.Failed.rawValue, error.localizedDescription)) + return } guard paymentMethodParams != nil else { resolve(Errors.createError(ConfirmPaymentErrorType.Unknown.rawValue, "Unhandled error occured")) @@ -600,10 +605,10 @@ public class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSele paymentIntentParams.paymentMethodParams = paymentMethodParams paymentIntentParams.paymentMethodOptions = paymentMethodOptions paymentIntentParams.shipping = Mappers.mapToShippingDetails(shippingDetails: params["shippingDetails"] as? NSDictionary) - - if let urlScheme = urlScheme { - paymentIntentParams.returnURL = Mappers.mapToReturnURL(urlScheme: urlScheme) - } + } + + if let urlScheme = urlScheme { + paymentIntentParams.returnURL = Mappers.mapToReturnURL(urlScheme: urlScheme) } let paymentHandler = STPPaymentHandler.shared() diff --git a/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart b/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart index cafcc4ebc..2814dc4f4 100644 --- a/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart +++ b/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart @@ -72,13 +72,13 @@ class MethodChannelStripe extends StripePlatform { } @override - Future confirmPaymentMethod( + Future confirmPayment( String paymentIntentClientSecret, PaymentMethodParams params, [ Map options = const {}, ]) async { final result = await _methodChannel - .invokeMapMethod('confirmPaymentMethod', { + .invokeMapMethod('confirmPayment', { 'paymentIntentClientSecret': paymentIntentClientSecret, 'params': params.toJson(), 'options': options, @@ -198,17 +198,12 @@ class MethodChannelStripe extends StripePlatform { @override Future createToken(CreateTokenParams params) async { - try { - final result = await _methodChannel.invokeMapMethod( - 'createToken', {'params': params.toJson()}); - - return TokenData.fromJson(result.unfoldToNonNull()); - } on Exception catch (e) { - throw StripeError( - code: CreateTokenError.unknown, - message: 'Create token failed with exception: $e', - ); - } + final result = await _methodChannel.invokeMapMethod( + 'createToken', {'params': params.toJson()}); + + return ResultParser( + parseJson: (json) => TokenData.fromJson(json)) + .parse(result: result!, successResultKey: 'token'); } void _parsePaymentSheetResult(Map? result) { diff --git a/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart b/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart index 6bfd96a17..b4bfa736d 100644 --- a/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart +++ b/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart @@ -42,7 +42,7 @@ abstract class StripePlatform extends PlatformInterface { ]); Future handleCardAction(String paymentIntentClientSecret); - Future confirmPaymentMethod( + Future confirmPayment( String paymentIntentClientSecret, PaymentMethodParams params, [Map options = const {}]); Future isApplePaySupported() async => false; diff --git a/packages/stripe_platform_interface/test/method_channel_stripe_test.dart b/packages/stripe_platform_interface/test/method_channel_stripe_test.dart index cd29457b5..ed48f5ddb 100644 --- a/packages/stripe_platform_interface/test/method_channel_stripe_test.dart +++ b/packages/stripe_platform_interface/test/method_channel_stripe_test.dart @@ -111,14 +111,14 @@ void main() { platformIsIos: true, methodChannel: MethodChannelMock( channelName: methodChannelName, - method: 'confirmPaymentMethod', + method: 'confirmPayment', result: { "paymentIntent": PaymentIntentTestInstance.create('id1').toJsonMap() }, ).methodChannel, ); - result = await sut.confirmPaymentMethod( + result = await sut.confirmPayment( 'secret', const PaymentMethodParams.card()); }); @@ -133,7 +133,7 @@ void main() { platformIsIos: true, methodChannel: MethodChannelMock( channelName: methodChannelName, - method: 'confirmPaymentMethod', + method: 'confirmPayment', result: createErrorResponse('whoops'), ).methodChannel, ); @@ -141,7 +141,7 @@ void main() { test('It returns error', () async { expectLater( - () async => await sut.confirmPaymentMethod( + () async => await sut.confirmPayment( 'secret', const PaymentMethodParams.card(), ), @@ -502,10 +502,10 @@ void main() { sut = MethodChannelStripe( platformIsIos: false, methodChannel: MethodChannelMock( - channelName: methodChannelName, - method: 'createToken', - result: Exception('whoops')) - .methodChannel, + channelName: methodChannelName, + method: 'createToken', + result: createErrorResponse('whoops'), + ).methodChannel, ); }); @@ -513,7 +513,7 @@ void main() { expect( () async => await sut.createToken(params), throwsA( - const TypeMatcher>(), + const TypeMatcher(), )); }); }); diff --git a/packages/stripe_platform_interface/test/test_data.dart b/packages/stripe_platform_interface/test/test_data.dart index a4d60a3d9..f21cabd5d 100644 --- a/packages/stripe_platform_interface/test/test_data.dart +++ b/packages/stripe_platform_interface/test/test_data.dart @@ -132,18 +132,20 @@ extension TokenDataTestInstance on TokenData { ); Map jsonMap() => { - 'id': id, - 'livemode': livemode, - 'type': describeEnum(type), - 'created': createdDateTime, - 'card': { - 'brand': card?.brand, - 'country': card?.country, - 'expYear': card?.expYear, - 'expMonth': card?.expMonth, - 'funding': card?.funding, - 'last4': card?.last4, - }, + 'token': { + 'id': id, + 'livemode': livemode, + 'type': describeEnum(type), + 'created': createdDateTime, + 'card': { + 'brand': card?.brand, + 'country': card?.country, + 'expYear': card?.expYear, + 'expMonth': card?.expMonth, + 'funding': card?.funding, + 'last4': card?.last4, + }, + } }; }