From 9067141be6a9d6402e61fc62b519f117bded00d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mon?= Date: Mon, 16 Oct 2023 14:37:40 +0200 Subject: [PATCH] Sync with v0.33 (#1444) * Sync Android with 0.33.0 * Sync iOS with 0.33.0 * feat: add new endpoints to example server * fix: add label and amount for google pay * feat: add customersheet to stripe sdk * chore: add customer sheet to example app * DOC: customer sheet * fix android integration test * fix stripe js token creation test --------- Co-authored-by: Jonas Bark Co-authored-by: Remon <> --- README.md | 2 + docs.json | 3 + docs/customer_sheet.mdx | 186 +++ .../com/flutter/stripe/example/BridgeTest.kt | 2 +- .../customer_sheet/customer_sheet_screen.dart | 141 +++ example/lib/screens/screens.dart | 8 + example/server/src/index.ts | 94 ++ packages/stripe/README.md | 2 + packages/stripe/lib/src/stripe.dart | 23 + packages/stripe_android/android/build.gradle | 2 +- .../com/flutter/stripe/StripeAndroidPlugin.kt | 53 + .../GooglePayLauncherFragment.kt | 8 +- .../GooglePayRequestHelper.kt | 10 + .../PaymentLauncherFragment.kt | 2 + .../PaymentSheetFragment.kt | 19 +- .../reactnativestripesdk/StripeSdkModule.kt | 153 ++- .../customersheet/CustomerSheetFragment.kt | 329 ++++++ .../ReactNativeCustomerAdapter.kt | 138 +++ .../com/reactnativestripesdk/utils/Mappers.kt | 38 +- .../Stripe Sdk/ApplePayViewController.swift | 29 +- .../CustomerSheet/CustomerSheetUtils.swift | 181 +++ .../ReactNativeCustomerAdapter.swift | 173 +++ .../ios/Classes/Stripe Sdk/Mappers.swift | 20 +- .../Stripe Sdk/StripeSdk+CustomerSheet.swift | 166 +++ .../Stripe Sdk/StripeSdk+PaymentSheet.swift | 8 +- .../ios/Classes/Stripe Sdk/StripeSdk.swift | 41 +- .../stripe_ios/ios/Classes/StripePlugin.swift | 87 ++ packages/stripe_ios/ios/stripe_ios.podspec | 2 +- .../test/src/js/tokens/create_token_test.dart | 1 + .../lib/src/method_channel_stripe.dart | 69 ++ .../lib/src/models/customer_sheet.dart | 116 ++ .../src/models/customer_sheet.freezed.dart | 1044 +++++++++++++++++ .../lib/src/models/customer_sheet.g.dart | 121 ++ .../lib/src/models/errors.dart | 2 + .../lib/src/models/google_pay.dart | 6 + .../lib/src/models/google_pay.freezed.dart | 68 +- .../lib/src/models/google_pay.g.dart | 4 + .../lib/src/models/payment_sheet.dart | 9 + .../lib/src/models/payment_sheet.freezed.dart | 123 +- .../lib/src/models/payment_sheet.g.dart | 8 + .../lib/src/stripe_platform_interface.dart | 11 + .../lib/stripe_platform_interface.dart | 1 + .../test/method_channel_stripe_test.dart | 50 +- packages/stripe_web/lib/src/web_stripe.dart | 18 + 44 files changed, 3493 insertions(+), 78 deletions(-) create mode 100644 docs/customer_sheet.mdx create mode 100644 example/lib/screens/customer_sheet/customer_sheet_screen.dart create mode 100644 packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/CustomerSheetFragment.kt create mode 100644 packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/ReactNativeCustomerAdapter.kt create mode 100644 packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift create mode 100644 packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift create mode 100644 packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift create mode 100644 packages/stripe_platform_interface/lib/src/models/customer_sheet.dart create mode 100644 packages/stripe_platform_interface/lib/src/models/customer_sheet.freezed.dart create mode 100644 packages/stripe_platform_interface/lib/src/models/customer_sheet.g.dart diff --git a/README.md b/README.md index 6f0f11a91..bb14fb34b 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ Payment sheet | Easy | Our recommended way of handling payments. It off Cardfield | Medium | Single line cardfield. Offers more flexibility but has less built-in functionality. | [docs](https://docs.page/flutter-stripe/flutter_stripe/card_field) | Card form | Medium | Similar as the cardfield but the entry fields are spread across multi lines | [docs](https://docs.page/flutter-stripe/flutter_stripe/card_field) | +### Mobile elements [BETA] +We also support the customer sheet mobile element. Check out the [docs](https://docs.page/flutter-stripe/flutter_stripe/customer_sheet) to learn more on how to set it up. ### Financial connections We also support Financial connections in our latest sdk. Check out the [docs](https://docs.page/flutter-stripe/flutter_stripe/financial_connections) to learn more on how to set it up. diff --git a/docs.json b/docs.json index 58d046c99..ebd8a89c1 100644 --- a/docs.json +++ b/docs.json @@ -11,6 +11,9 @@ ["CardFormField", "/card_field"] ] ], + ["Mobile elements", + ["Customer Sheet", "/customer_sheet"] + ], ["Regional payments", [ ["ACH direct debit", "/ach"], ["Ideal", "/ideal"] diff --git a/docs/customer_sheet.mdx b/docs/customer_sheet.mdx new file mode 100644 index 000000000..0dc23e636 --- /dev/null +++ b/docs/customer_sheet.mdx @@ -0,0 +1,186 @@ +--- +title: Customer sheet +description: Offer a pre-built UI for your customers to manage their saved payment methods. +--- + +# Mobile elements - Customer Sheet + +### Let customers manage their own payments. + + + +Important the customer sheet is in Beta so it is not guaranteed to work under all circumstances. + +## 1. Set up Stripe [Server Side] [Client Side] + +First, you need a Stripe account. [Register now](https://dashboard.stripe.com/register). + +#### Server-side + +This integration requires endpoints on your server that talk to the Stripe API. Use one official libraries for access to the Stripe API from your server. [Follow the steps here](https://stripe.com/docs/payments/accept-a-payment?platform=ios&ui=payment-sheet#setup-server-side) + +#### Client-side + +The Flutter SDK is open source, fully documented. + +To install the SDK, follow these steps: + - Run the commmand `flutter pub add flutter_stripe` + - This will add a line like this to your project's pubspec.yaml with the latest package version + + +For details on the latest SDK release and past versions, see the [Releases](https://github.com/flutter-stripe/flutter_stripe/releases) page on GitHub. To receive notifications when a new release is published, [watch releases for the repository](https://docs.github.com/en/github/managing-subscriptions-and-notifications-on-github/managing-subscriptions-for-activity-on-github/viewing-your-subscriptions#watching-releases-for-a-repository). + + +When your app starts, configure the SDK with your Stripe [publishable key](https://dashboard.stripe.com/) so that it can make requests to the Stripe API. + +```dart +void main() async { + Stripe.publishableKey = stripePublishableKey; + runApp(const App()); +} +``` + +Use your [test mode](https://stripe.com/docs/keys#obtain-api-keys) keys while you test and develop, and your [live mode](https://stripe.com/docs/keys#test-live-modes) keys when you publish your app. + +## 2. Add an enpoint [Server Side] + +First, you need a Stripe account. [Register now](https://dashboard.stripe.com/register). + +#### Server-side + +This integration uses three Stripe API objects: + +1. A [PaymentIntent](https://stripe.com/docs/api/payment_intents). Stripe uses this to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process. + +2. A [Customer](https://stripe.com/docs/api/customers) (optional). To set up a card for future payments, it must be attached to a Customer. Create a Customer object when your customer creates an account with your business. If your customer is making a payment as a guest, you can create a Customer object before payment and associate it with your own internal representation of the customer’s account later. + +3. A Customer Ephemeral Key (optional). Information on the Customer object is sensitive, and can’t be retrieved directly from an app. An Ephemeral Key grants the SDK temporary access to the Customer. + +>> If you never save cards to a Customer and don’t allow returning Customers to reuse saved cards, you can omit the Customer and Customer Ephemeral Key objects from your integration. + +For security reasons, your app can’t create these objects. Instead, add an endpoint on your server that: + +1. Retrieves the Customer, or creates a new one. +2. Creates an Ephemeral Key for the Customer. +3. Creates a PaymentIntent, passing the Customer id. +4. Returns the Payment Intent’s [client secret](https://stripe.com/docs/api/payment_intents/object#payment_intent_object-client_secret), the Ephemeral Key’s secret, and the Customer’s id to your app. + +Check examples implementations for your server [here](https://stripe.com/docs/payments/accept-a-payment?platform=ios#add-server-endpoint) + +## 3. Collect customer details [Client Side] + +First initialize the customer sheet + +```dart + Future initCustomerSheet() async { + try { + // 1. retrieve customer from backend. + final data = await _createTestCustomerSheet(); + + // create some billingdetails + final billingDetails = BillingDetails( + name: 'Flutter Stripe', + 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. initialize the customer sheet + await Stripe.instance.initCustomerSheet( + customerSheetInitParams: CustomerSheetInitParams( + // Main params + setupIntentClientSecret: data['setupIntent'], + merchantDisplayName: 'Flutter Stripe Store Demo', + // Customer params + customerId: data['customer'], + customerEphemeralKeySecret: data['ephemeralKeySecret'], + style: ThemeMode.system, + defaultBillingDetails: billingDetails, + ), + ); + setState(() { + step = 1; + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + rethrow; + } + } + ``` + + Then you can show the customer sheet + + ```dart + Future confirmCustomerSheet() async { + try { + // 3. display the customer sheet. + final result = await Stripe.instance.presentCustomerSheet(); + + setState(() { + step = 0; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Payment succesfully modified option selected: ${result?.paymentOption?.label}}'), + ), + ); + } on Exception catch (e) { + if (e is StripeException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error from Stripe: ${e.error.localizedMessage}'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Unforeseen error: ${e}'), + ), + ); + } + } + } + ``` + +## 3. Optional configuration + +### Change appearance + +It is possible to change the appearance of the customer sheet. + +```dart + await Stripe.instance.initCustomerSheet( + appearance: PaymentSheetAppearance( + colors: PaymentSheetAppearanceColors( + background: Colors.lightBlue, + primary: Colors.blue, + componentBorder: Colors.red, + ), + shapes: PaymentSheetShape( + borderWidth: 4, + shadow: PaymentSheetShadowParams(color: Colors.red), + ), + primaryButton: PaymentSheetPrimaryButtonAppearance( + shapes: PaymentSheetPrimaryButtonShape(blurRadius: 8), + colors: PaymentSheetPrimaryButtonTheme( + light: PaymentSheetPrimaryButtonThemeColors( + background: Color.fromARGB(255, 231, 235, 30), + text: Color.fromARGB(255, 235, 92, 30), + border: Color.fromARGB(255, 235, 92, 30), + ), + ), + ), + ), +); +``` diff --git a/example/android/app/src/androidTest/java/com/flutter/stripe/example/BridgeTest.kt b/example/android/app/src/androidTest/java/com/flutter/stripe/example/BridgeTest.kt index 349a85e02..49588e74c 100644 --- a/example/android/app/src/androidTest/java/com/flutter/stripe/example/BridgeTest.kt +++ b/example/android/app/src/androidTest/java/com/flutter/stripe/example/BridgeTest.kt @@ -6,7 +6,7 @@ class BridgeTest { @Test fun testGetDouble() { val map = ReadableMap(mapOf("test" to 1.1)) - assert(map.getDouble("test") == 1.1f) + assert(map.getDouble("test") == 1.1) } @Test fun testGetIntShouldFail() { diff --git a/example/lib/screens/customer_sheet/customer_sheet_screen.dart b/example/lib/screens/customer_sheet/customer_sheet_screen.dart new file mode 100644 index 000000000..4410a98d6 --- /dev/null +++ b/example/lib/screens/customer_sheet/customer_sheet_screen.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:http/http.dart' as http; +import 'package:stripe_example/config.dart'; +import 'package:stripe_example/widgets/example_scaffold.dart'; +import 'package:stripe_example/widgets/loading_button.dart'; + +class CustomerSheetScreen extends StatefulWidget { + @override + _CustomerSheetScreenState createState() => _CustomerSheetScreenState(); +} + +class _CustomerSheetScreenState extends State { + int step = 0; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + title: 'Customer Sheet', + tags: ['Single Step'], + children: [ + Stepper( + controlsBuilder: (_, __) => SizedBox(), + currentStep: step, + steps: [ + Step( + title: Text('Init customer sheet'), + content: LoadingButton( + onPressed: initCustomerSheet, + text: 'Init customer sheet', + ), + ), + Step( + title: Text('Confirm customer sheet'), + content: LoadingButton( + onPressed: confirmCustomerSheet, + text: 'Select payment method now', + ), + ), + ], + ), + ], + ); + } + + Future> _createTestCustomerSheet() async { + final url = Uri.parse('$kApiUrl/customer-sheet'); + final response = await http.post( + url, + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'a': 'a', + }), + ); + final body = json.decode(response.body); + if (body['error'] != null) { + throw Exception(body['error']); + } + return body; + } + + Future initCustomerSheet() async { + try { + // 1. retrieve customer from backend. + final data = await _createTestCustomerSheet(); + + // create some billingdetails + final billingDetails = BillingDetails( + name: 'Flutter Stripe', + 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. initialize the customer sheet + await Stripe.instance.initCustomerSheet( + customerSheetInitParams: CustomerSheetInitParams( + // Main params + setupIntentClientSecret: data['setupIntent'], + merchantDisplayName: 'Flutter Stripe Store Demo', + // Customer params + customerId: data['customer'], + customerEphemeralKeySecret: data['ephemeralKeySecret'], + style: ThemeMode.system, + defaultBillingDetails: billingDetails, + ), + ); + setState(() { + step = 1; + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + rethrow; + } + } + + Future confirmCustomerSheet() async { + try { + // 3. display the customer sheet. + final result = await Stripe.instance.presentCustomerSheet(); + + setState(() { + step = 0; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Payment preferences modfied completed option selected: ${result?.paymentOption?.label}}'), + ), + ); + } on Exception catch (e) { + if (e is StripeException) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error from Stripe: ${e.error.localizedMessage}'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Unforeseen error: ${e}'), + ), + ); + } + } + } +} diff --git a/example/lib/screens/screens.dart b/example/lib/screens/screens.dart index c35679b2d..d6d26170e 100644 --- a/example/lib/screens/screens.dart +++ b/example/lib/screens/screens.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stripe_example/screens/checkout/checkout_screen.dart'; +import 'package:stripe_example/screens/customer_sheet/customer_sheet_screen.dart'; import 'package:stripe_example/screens/payment_sheet/payment_element/payment_element.dart'; import 'package:stripe_example/screens/payment_sheet/payment_sheet_deffered_screen.dart'; import 'package:stripe_example/screens/payment_sheet/payment_sheet_screen.dart'; @@ -122,6 +123,13 @@ class Example extends StatelessWidget { ], expanded: true, ), + ExampleSection(title: 'Customer sheet', children: [ + Example( + title: 'Customer sheet', + builder: (context) => CustomerSheetScreen(), + platformsSupported: [DevicePlatform.android, DevicePlatform.ios], + ), + ]), ExampleSection( title: 'Card Payments', children: [ diff --git a/example/server/src/index.ts b/example/server/src/index.ts index 0285ca022..ef36faddd 100644 --- a/example/server/src/index.ts +++ b/example/server/src/index.ts @@ -717,7 +717,97 @@ app.post('/create-checkout-session', async (req, res) => { const { port, }: { port?: string; } = req.body; + + const { secret_key } = getKeys(); + + const stripe = new Stripe(secret_key as string, { + apiVersion: '2023-08-16', + typescript: true, + }); + var effectivePort = port ?? 8080; + // Use an existing Customer ID if this is a returning customer. + const customer = await stripe.customers.create(); + + // Use the same version as the SDK + const ephemeralKey = await stripe.ephemeralKeys.create( + { customer: customer.id }, + { apiVersion: '2020-08-27' } + ); + + const setupIntent = await stripe.setupIntents.create({ + customer: customer.id, + }); + + res.json({ + customer: customer.id, + ephemeralKeySecret: ephemeralKey.secret, + setupIntent: setupIntent.client_secret, + }); +}); + +app.post('/fetch-payment-methods', async (req, res) => { + const { secret_key } = getKeys(); + + const stripe = new Stripe(secret_key as string, { + apiVersion: '2023-08-16', + typescript: true, + }); + + const paymentMethods = await stripe.customers.listPaymentMethods( + req.body.customerId + ); + + res.json({ + paymentMethods: paymentMethods.data, + }); +}); + +app.post('/attach-payment-method', async (req, res) => { + const { secret_key } = getKeys(); + + const stripe = new Stripe(secret_key as string, { + apiVersion: '2023-08-16', + typescript: true, + }); + console.log({ customer: req.body.customerId }); + const paymentMethod = await stripe.paymentMethods.attach( + req.body.paymentMethodId, + { customer: req.body.customerId } + ); + console.log('got here'); + res.json({ + paymentMethod, + }); +}); + +app.post('/detach-payment-method', async (req, res) => { + const { secret_key } = getKeys(); + + const stripe = new Stripe(secret_key as string, { + apiVersion: '2023-08-16', + typescript: true, + }); + + const paymentMethod = await stripe.paymentMethods.detach( + req.body.paymentMethodId + ); + + res.json({ + paymentMethod, + }); +}); + +// Mocks a Database. In your code, you should use a persistent database. +let savedPaymentOptions = new Map(); + +app.post('/set-payment-option', async (req, res) => { + savedPaymentOptions.set(req.body.customerId, req.body.paymentOption); + res.json({}); +}); + +app.post('/get-payment-option', async (req, res) => { + const { secret_key } = getKeys(); const stripe = new Stripe(secret_key as string, { @@ -725,6 +815,10 @@ app.post('/create-checkout-session', async (req, res) => { typescript: true, }); + const customerPaymentOption = savedPaymentOptions.get(req.body.customerId); + res.json({ + savedPaymentOption: customerPaymentOption ?? null, + }); const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ diff --git a/packages/stripe/README.md b/packages/stripe/README.md index 6f0f11a91..bb14fb34b 100644 --- a/packages/stripe/README.md +++ b/packages/stripe/README.md @@ -92,6 +92,8 @@ Payment sheet | Easy | Our recommended way of handling payments. It off Cardfield | Medium | Single line cardfield. Offers more flexibility but has less built-in functionality. | [docs](https://docs.page/flutter-stripe/flutter_stripe/card_field) | Card form | Medium | Similar as the cardfield but the entry fields are spread across multi lines | [docs](https://docs.page/flutter-stripe/flutter_stripe/card_field) | +### Mobile elements [BETA] +We also support the customer sheet mobile element. Check out the [docs](https://docs.page/flutter-stripe/flutter_stripe/customer_sheet) to learn more on how to set it up. ### Financial connections We also support Financial connections in our latest sdk. Check out the [docs](https://docs.page/flutter-stripe/flutter_stripe/financial_connections) to learn more on how to set it up. diff --git a/packages/stripe/lib/src/stripe.dart b/packages/stripe/lib/src/stripe.dart index dea363011..b608132f7 100644 --- a/packages/stripe/lib/src/stripe.dart +++ b/packages/stripe/lib/src/stripe.dart @@ -619,6 +619,29 @@ class Stripe { } } + /// Initializes the customer sheet with the provided [parameters]. + Future initCustomerSheet( + {required CustomerSheetInitParams customerSheetInitParams}) async { + await _awaitForSettings(); + return _platform.initCustomerSheet(customerSheetInitParams); + } + + /// Display the customersheet sheet. With the provided [options]. + Future presentCustomerSheet({ + CustomerSheetPresentParams? options, + }) async { + await _awaitForSettings(); + return _platform.presentCustomerSheet(options: options); + } + + /// Retrieve the customer sheet payment option selection. + Future + retrieveCustomerSheetPaymentOptionSelection() async { + await _awaitForSettings(); + + return _platform.retrieveCustomerSheetPaymentOptionSelection(); + } + FutureOr _awaitForSettings() { if (_needsSettings) { _settingsFuture = applySettings(); diff --git a/packages/stripe_android/android/build.gradle b/packages/stripe_android/android/build.gradle index 69f8682e1..b2c352c99 100644 --- a/packages/stripe_android/android/build.gradle +++ b/packages/stripe_android/android/build.gradle @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.8.0' - ext.stripe_version = '20.28.+' + ext.stripe_version = '20.31.+' repositories { google() 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 2a06248aa..41a413ae4 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 @@ -208,6 +208,59 @@ If you continue to have trouble, follow this discussion to get some support http stripeSdk.removeListeners(count = call.requiredArgument("count")) result.success("OK") } + "initCustomerSheet" -> { + stripeSdk.initCustomerSheet( + params = call.requiredArgument("params"), + customerAdapterOverrides = call.requiredArgument("customerAdapterOverrides"), + promise = Promise(result) + ) + } + "presentCustomerSheet" -> { + stripeSdk.presentCustomerSheet( + params = call.requiredArgument("params"), + promise = Promise(result) + ) + } + "retrieveCustomerSheetPaymentOptionSelection" -> { + stripeSdk.retrieveCustomerSheetPaymentOptionSelection( + promise = Promise(result) + ) + } + "customerAdapterFetchPaymentMethodsCallback" -> { + stripeSdk.customerAdapterFetchPaymentMethodsCallback( + paymentMethodJsonObjects = call.requiredArgument("paymentMethodJsonObjects"), + promise = Promise(result) + ) + } + "customerAdapterAttachPaymentMethodCallback" -> { + stripeSdk.customerAdapterAttachPaymentMethodCallback( + paymentMethodJson = call.requiredArgument("paymentMethodJson"), + promise = Promise(result) + ) + } + "customerAdapterDetachPaymentMethodCallback" -> { + stripeSdk.customerAdapterDetachPaymentMethodCallback( + paymentMethodJson = call.requiredArgument("paymentMethodJson"), + promise = Promise(result) + ) + } + "customerAdapterSetSelectedPaymentOptionCallback" -> { + stripeSdk.customerAdapterSetSelectedPaymentOptionCallback( + promise = Promise(result) + ) + } + "customerAdapterFetchSelectedPaymentOptionCallback" -> { + stripeSdk.customerAdapterFetchSelectedPaymentOptionCallback( + paymentOption = call.optionalArgument("paymentOption"), + promise = Promise(result) + ) + } + "customerAdapterSetupIntentClientSecretForCustomerAttachCallback" -> { + stripeSdk.customerAdapterSetupIntentClientSecretForCustomerAttachCallback( + clientSecret = call.requiredArgument("clientSecret"), + promise = Promise(result) + ) + } else -> result.notImplemented() } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayLauncherFragment.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayLauncherFragment.kt index 6fa090ff9..54a249a01 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayLauncherFragment.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayLauncherFragment.kt @@ -24,6 +24,8 @@ class GooglePayLauncherFragment : Fragment() { private lateinit var mode: Mode private lateinit var configuration: GooglePayLauncher.Config private lateinit var currencyCode: String + private var amount: Int? = null + private var label: String? = null private lateinit var callback: (result: GooglePayLauncher.Result?, error: WritableMap?) -> Unit override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, @@ -47,6 +49,8 @@ class GooglePayLauncherFragment : Fragment() { this.mode = mode this.callback = callback this.currencyCode = googlePayParams.getString("currencyCode") ?: "USD" + this.amount = googlePayParams.getInt("amount") + this.label = googlePayParams.getString("label") this.configuration = GooglePayLauncher.Config( environment = if (googlePayParams.getBoolean("testEnv")) GooglePayEnvironment.Test else GooglePayEnvironment.Production, merchantCountryCode = googlePayParams.getString("merchantCountryCode").orEmpty(), @@ -89,10 +93,10 @@ class GooglePayLauncherFragment : Fragment() { if (isReady) { when (mode) { Mode.ForSetup -> { - launcher.presentForSetupIntent(clientSecret, currencyCode) + launcher.presentForSetupIntent(clientSecret, currencyCode, amount?.toLong(), label) } Mode.ForPayment -> { - launcher.presentForPaymentIntent(clientSecret) + launcher.presentForPaymentIntent(clientSecret, label) } } } else { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayRequestHelper.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayRequestHelper.kt index 94975019f..dcbb738ca 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayRequestHelper.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/GooglePayRequestHelper.kt @@ -80,12 +80,14 @@ class GooglePayRequestHelper { val countryCode = params.getString("merchantCountryCode").orEmpty() val currencyCode = params.getString("currencyCode") ?: "USD" val amount = params.getInt("amount") + val label = params.getString("label") return GooglePayJsonFactory.TransactionInfo( currencyCode = currencyCode, totalPriceStatus = GooglePayJsonFactory.TransactionInfo.TotalPriceStatus.Estimated, countryCode = countryCode, totalPrice = amount, + totalPriceLabel = label, checkoutOption = GooglePayJsonFactory.TransactionInfo.CheckoutOption.Default ) } @@ -134,6 +136,11 @@ class GooglePayRequestHelper { override fun onSuccess(result: PaymentMethod) { promiseResult.putMap("paymentMethod", mapFromPaymentMethod(result)) + GooglePayResult.fromJson(paymentInformation).let { + if (it.shippingInformation != null) { + promiseResult.putMap("shippingContact", mapFromShippingContact(it)) + } + } promise.resolve(promiseResult) } } @@ -146,6 +153,9 @@ class GooglePayRequestHelper { val promiseResult = WritableNativeMap() googlePayResult.token?.let { promiseResult.putMap("token", mapFromToken(it)) + if (googlePayResult.shippingInformation != null) { + promiseResult.putMap("shippingContact", mapFromShippingContact(googlePayResult)) + } promise.resolve(promiseResult) } ?: run { promise.resolve(createError("Failed", "Unexpected response from Google Pay. No token was found.")) diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentLauncherFragment.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentLauncherFragment.kt index f7224101c..114d9b29e 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentLauncherFragment.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentLauncherFragment.kt @@ -282,6 +282,8 @@ class PaymentLauncherFragment( private fun isNextActionSuccessState(nextAction: StripeIntent.NextActionType?): Boolean { return when (nextAction) { StripeIntent.NextActionType.DisplayOxxoDetails, + StripeIntent.NextActionType.DisplayBoletoDetails, + StripeIntent.NextActionType.DisplayKonbiniDetails, StripeIntent.NextActionType.VerifyWithMicrodeposits -> true StripeIntent.NextActionType.RedirectToUrl, StripeIntent.NextActionType.UseStripeSdk, 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 a97456c17..b6da5681e 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 @@ -6,6 +6,7 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color +import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler import android.os.Looper @@ -353,11 +354,15 @@ class PaymentSheetFragment( val countryCode = params.getString("merchantCountryCode").orEmpty() val currencyCode = params.getString("currencyCode").orEmpty() val testEnv = params.getBoolean("testEnv") + val amount = params.getString("amount")?.toLongOrNull() + val label = params.getString("label") return PaymentSheet.GooglePayConfiguration( environment = if (testEnv) PaymentSheet.GooglePayConfiguration.Environment.Test else PaymentSheet.GooglePayConfiguration.Environment.Production, countryCode = countryCode, - currencyCode = currencyCode + currencyCode = currencyCode, + amount = amount, + label = label ) } @@ -399,10 +404,16 @@ class PaymentSheetFragment( } fun getBitmapFromVectorDrawable(context: Context?, drawableId: Int): Bitmap? { - var drawable = AppCompatResources.getDrawable(context!!, drawableId) ?: return null + val drawable = AppCompatResources.getDrawable(context!!, drawableId) ?: return null + return getBitmapFromDrawable(drawable) +} - drawable = DrawableCompat.wrap(drawable).mutate() - val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) +fun getBitmapFromDrawable(drawable: Drawable): Bitmap? { + val drawableCompat = DrawableCompat.wrap(drawable).mutate() + if (drawableCompat.intrinsicWidth <= 0 || drawableCompat.intrinsicHeight <= 0) { + return null + } + val bitmap = Bitmap.createBitmap(drawableCompat.intrinsicWidth, drawableCompat.intrinsicHeight, Bitmap.Config.ARGB_8888) bitmap.eraseColor(Color.WHITE) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) 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 42e6d0249..f11aeeb4a 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 @@ -8,6 +8,7 @@ import androidx.fragment.app.FragmentActivity import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule +import com.flutter.stripe.activityResultRegistry import com.flutter.stripe.invoke import com.reactnativestripesdk.addresssheet.AddressLauncherFragment import com.reactnativestripesdk.pushprovisioning.PushProvisioningProxy @@ -23,6 +24,7 @@ import com.stripe.android.view.AddPaymentMethodActivityStarter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.json.JSONObject @ReactModule(name = StripeSdkModule.NAME) @@ -48,6 +50,8 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB private var paymentLauncherFragment: PaymentLauncherFragment? = null private var collectBankAccountLauncherFragment: CollectBankAccountLauncherFragment? = null + private var customerSheetFragment: CustomerSheetFragment? = null + internal var eventListenerCount = 0 // If you create a new Fragment, you must put the tag here, otherwise result callbacks for that @@ -59,7 +63,8 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB CollectBankAccountLauncherFragment.TAG, FinancialConnectionsSheetFragment.TAG, AddressLauncherFragment.TAG, - GooglePayLauncherFragment.TAG + GooglePayLauncherFragment.TAG, + CustomerSheetFragment.TAG ) private val mActivityEventListener = object : BaseActivityEventListener() { @@ -73,6 +78,7 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB } ?: run { Log.d("StripeReactNative", "No promise was found, Google Pay result went unhandled,") } } else -> { + dispatchActivityResultsToFragments(requestCode, resultCode, data) try { val result = AddPaymentMethodActivityStarter.Result.fromIntent(data) if (data?.getParcelableExtra("extra_activity_result") != null) { @@ -91,6 +97,17 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB reactContext.addActivityEventListener(mActivityEventListener) } + // Necessary on older versions of React Native (~0.65 and below) + private fun dispatchActivityResultsToFragments(requestCode: Int, resultCode: Int, data: Intent?) { + getCurrentActivityOrResolveWithError(null)?.supportFragmentManager?.let { fragmentManager -> + for (tag in allStripeFragmentTags) { + fragmentManager.findFragmentByTag(tag)?.let { + it.activity?.activityResultRegistry?.dispatchResult(requestCode, resultCode, data) + } + } + } + } + private fun configure3dSecure(params: ReadableMap) { val stripe3dsConfigBuilder = PaymentAuthConfig.Stripe3ds2Config.Builder() if (params.hasKey("timeout")) stripe3dsConfigBuilder.setTimeout(params.getInt("timeout")) @@ -490,11 +507,7 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB fun retrievePaymentIntent(clientSecret: String, promise: Promise) { CoroutineScope(Dispatchers.IO).launch { val paymentIntent = stripe.retrievePaymentIntentSynchronous(clientSecret) - paymentIntent?.let { - promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(it))) - } ?: run { - promise.resolve(createError(RetrievePaymentIntentErrorType.Unknown.toString(), "Failed to retrieve the PaymentIntent")) - } + promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(paymentIntent))) } } @@ -502,11 +515,7 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB fun retrieveSetupIntent(clientSecret: String, promise: Promise) { CoroutineScope(Dispatchers.IO).launch { val setupIntent = stripe.retrieveSetupIntentSynchronous(clientSecret) - setupIntent?.let { - promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(it))) - } ?: run { - promise.resolve(createError(RetrieveSetupIntentErrorType.Unknown.toString(), "Failed to retrieve the SetupIntent")) - } + promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(setupIntent))) } } @@ -809,6 +818,128 @@ class StripeSdkModule(val reactContext: ReactApplicationContext) : ReactContextB } } + @ReactMethod + fun initCustomerSheet(params: ReadableMap, customerAdapterOverrides: ReadableMap, promise: Promise) { + if (!::stripe.isInitialized) { + promise.resolve(createMissingInitError()) + return + } + + getCurrentActivityOrResolveWithError(promise)?.let { activity -> + customerSheetFragment?.removeFragment(reactApplicationContext) + customerSheetFragment = CustomerSheetFragment().also { + it.context = reactApplicationContext + it.initPromise = promise + val bundle = toBundleObject(params) + bundle.putBundle("customerAdapter", toBundleObject(customerAdapterOverrides)) + it.arguments = bundle + } + try { + activity.supportFragmentManager.beginTransaction() + .add(customerSheetFragment!!, CustomerSheetFragment.TAG) + .commit() + } catch (error: IllegalStateException) { + promise.resolve(createError(ErrorType.Failed.toString(), error.message)) + } + } + } + + @ReactMethod + fun presentCustomerSheet(params: ReadableMap, promise: Promise) { + var timeout: Long? = null + if (params.hasKey("timeout")) { + timeout = params.getInt("timeout").toLong() + } + customerSheetFragment?.present(timeout, promise) ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + } + } + + @ReactMethod + fun retrieveCustomerSheetPaymentOptionSelection(promise: Promise) { + customerSheetFragment?.retrievePaymentOptionSelection(promise) ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + } + } + + @ReactMethod + fun customerAdapterFetchPaymentMethodsCallback(paymentMethodJsonObjects: ReadableArray, promise: Promise) { + customerSheetFragment?.let { fragment -> + val paymentMethods = mutableListOf() + for (paymentMethodJson in paymentMethodJsonObjects.toArrayList()) { + PaymentMethod.fromJson(JSONObject((paymentMethodJson as HashMap<*, *>)))?.let { + paymentMethods.add(it) + } ?: run { + Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method") + } + } + fragment.customerAdapter?.fetchPaymentMethodsCallback?.complete(paymentMethods) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + + @ReactMethod + fun customerAdapterAttachPaymentMethodCallback(paymentMethodJson: ReadableMap, promise: Promise) { + customerSheetFragment?.let { + val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap())) + if (paymentMethod == null) { + Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method") + return + } + it.customerAdapter?.attachPaymentMethodCallback?.complete(paymentMethod) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + + @ReactMethod + fun customerAdapterDetachPaymentMethodCallback(paymentMethodJson: ReadableMap, promise: Promise) { + customerSheetFragment?.let { + val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap())) + if (paymentMethod == null) { + Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method") + return + } + it.customerAdapter?.detachPaymentMethodCallback?.complete(paymentMethod) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + + @ReactMethod + fun customerAdapterSetSelectedPaymentOptionCallback(promise: Promise) { + customerSheetFragment?.let { + it.customerAdapter?.setSelectedPaymentOptionCallback?.complete(Unit) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + + @ReactMethod + fun customerAdapterFetchSelectedPaymentOptionCallback(paymentOption: String?, promise: Promise) { + customerSheetFragment?.let { + it.customerAdapter?.fetchSelectedPaymentOptionCallback?.complete(paymentOption) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + + @ReactMethod + fun customerAdapterSetupIntentClientSecretForCustomerAttachCallback(clientSecret: String, promise: Promise) { + customerSheetFragment?.let { + it.customerAdapter?.setupIntentClientSecretForCustomerAttachCallback?.complete(clientSecret) + } ?: run { + promise.resolve(CustomerSheetFragment.createMissingInitError()) + return + } + } + @ReactMethod fun addListener(eventName: String) { eventListenerCount++ diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/CustomerSheetFragment.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/CustomerSheetFragment.kt new file mode 100644 index 000000000..f70fb99e7 --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/CustomerSheetFragment.kt @@ -0,0 +1,329 @@ +package com.reactnativestripesdk + +import android.app.Activity +import android.app.Application +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import com.facebook.react.bridge.* +import com.reactnativestripesdk.customersheet.ReactNativeCustomerAdapter +import com.reactnativestripesdk.utils.* +import com.stripe.android.customersheet.CustomerAdapter +import com.stripe.android.customersheet.CustomerEphemeralKey +import com.stripe.android.customersheet.CustomerSheet +import com.stripe.android.customersheet.CustomerSheetResult +import com.stripe.android.customersheet.ExperimentalCustomerSheetApi +import com.stripe.android.customersheet.PaymentOptionSelection +import com.stripe.android.model.PaymentMethod +import com.stripe.android.paymentsheet.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalCustomerSheetApi::class) +class CustomerSheetFragment : Fragment() { + private var customerSheet: CustomerSheet? = null + internal var customerAdapter: ReactNativeCustomerAdapter? = null + internal var context: ReactApplicationContext? = null + internal var initPromise: Promise? = null + private var presentPromise: Promise? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FrameLayout(requireActivity()).also { + it.visibility = View.GONE + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val context = context ?: run { + Log.e("StripeReactNative", "No context found during CustomerSheet.initialize. Please file an issue: https://github.com/stripe/stripe-react-native/issues") + return + } + val initPromise = initPromise ?: run { + Log.e("StripeReactNative", "No promise found for CustomerSheet.initialize. Please file an issue: https://github.com/stripe/stripe-react-native/issues") + return + } + + val headerTextForSelectionScreen = arguments?.getString("headerTextForSelectionScreen") + val merchantDisplayName = arguments?.getString("merchantDisplayName") + val googlePayEnabled = arguments?.getBoolean("googlePayEnabled") ?: false + val billingDetailsBundle = arguments?.getBundle("defaultBillingDetails") + val billingConfigParams = arguments?.getBundle("billingDetailsCollectionConfiguration") + val setupIntentClientSecret = arguments?.getString("setupIntentClientSecret") + val customerId = arguments?.getString("customerId") + val customerEphemeralKeySecret = arguments?.getString("customerEphemeralKeySecret") + val customerAdapterOverrideParams = arguments?.getBundle("customerAdapter") + + if (customerId == null) { + initPromise.resolve(createError(ErrorType.Failed.toString(), "You must provide a value for `customerId`")) + return + } + if (customerEphemeralKeySecret == null) { + initPromise.resolve(createError(ErrorType.Failed.toString(), "You must provide a value for `customerEphemeralKeySecret`")) + return + } + + val appearance = try { + buildPaymentSheetAppearance(arguments?.getBundle("appearance"), context) + } catch (error: PaymentSheetAppearanceException) { + initPromise.resolve(createError(ErrorType.Failed.toString(), error)) + return + } + + val configuration = CustomerSheet.Configuration.builder() + .appearance(appearance) + .googlePayEnabled(googlePayEnabled) + .merchantDisplayName(merchantDisplayName) + .headerTextForSelectionScreen(headerTextForSelectionScreen) + + billingDetailsBundle?.let { + configuration.defaultBillingDetails(createDefaultBillingDetails(billingDetailsBundle)) + } + billingConfigParams?.let { + configuration.billingDetailsCollectionConfiguration(createBillingDetailsCollectionConfiguration(billingConfigParams)) + } + + val customerAdapter = createCustomerAdapter( + context, customerId, customerEphemeralKeySecret, setupIntentClientSecret, customerAdapterOverrideParams + ).also { + this.customerAdapter = it + } + + customerSheet = CustomerSheet.create( + fragment = this, + configuration = configuration.build(), + customerAdapter = customerAdapter, + callback = ::handleResult + ) + + initPromise.resolve(WritableNativeMap()) + } + + private fun handleResult(result: CustomerSheetResult) { + val presentPromise = presentPromise ?: run { + Log.e("StripeReactNative", "No promise found for CustomerSheet.present") + return + } + + var promiseResult = Arguments.createMap() + when (result) { + is CustomerSheetResult.Failed -> { + presentPromise.resolve(createError(ErrorType.Failed.toString(), result.exception)) + } + is CustomerSheetResult.Selected -> { + promiseResult = createPaymentOptionResult(result.selection) + } + is CustomerSheetResult.Canceled -> { + promiseResult = createPaymentOptionResult(result.selection) + promiseResult.putMap("error", Arguments.createMap().also { it.putString("code", ErrorType.Canceled.toString()) }) + } + } + presentPromise.resolve(promiseResult) + } + + fun present(timeout: Long?, promise: Promise) { + presentPromise = promise + if (timeout != null) { + presentWithTimeout(timeout, promise) + } + customerSheet?.present() ?: run { + promise.resolve(createMissingInitError()) + } + } + + private fun presentWithTimeout(timeout: Long, promise: Promise) { + var customerSheetActivity: Activity? = null + var activities: MutableList = mutableListOf() + val activityLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + customerSheetActivity = activity + activities.add(activity) + } + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) { + customerSheetActivity = null + activities = mutableListOf() + context?.currentActivity?.application?.unregisterActivityLifecycleCallbacks(this) + } + } + + Handler(Looper.getMainLooper()).postDelayed({ + //customerSheetActivity?.finish() + for (a in activities) { + a.finish() + } + }, timeout) + + + context?.currentActivity?.application?.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + + customerSheet?.present() ?: run { + promise.resolve(createMissingInitError()) + } + } + + internal fun retrievePaymentOptionSelection(promise: Promise) { + CoroutineScope(Dispatchers.IO).launch { + runCatching { + val result = customerSheet?.retrievePaymentOptionSelection() ?: run { + promise.resolve(createMissingInitError()) + return@launch + } + var promiseResult = Arguments.createMap() + when (result) { + is CustomerSheetResult.Failed -> { + promise.resolve(createError(ErrorType.Failed.toString(), result.exception)) + } + is CustomerSheetResult.Selected -> { + promiseResult = createPaymentOptionResult(result.selection) + } + is CustomerSheetResult.Canceled -> { + promiseResult = createPaymentOptionResult(result.selection) + promiseResult.putMap("error", Arguments.createMap().also { it.putString("code", ErrorType.Canceled.toString()) }) + } + } + promise.resolve(promiseResult) + }.onFailure { + promise.resolve(createError(CreateTokenErrorType.Failed.toString(), it.message)) + } + } + } + + companion object { + internal const val TAG = "customer_sheet_launch_fragment" + + internal fun createMissingInitError(): WritableMap { + return createError(ErrorType.Failed.toString(), "No customer sheet has been initialized yet.") + } + + internal fun createDefaultBillingDetails(bundle: Bundle): PaymentSheet.BillingDetails { + val addressBundle = bundle.getBundle("address") + val address = PaymentSheet.Address( + addressBundle?.getString("city"), + addressBundle?.getString("country"), + addressBundle?.getString("line1"), + addressBundle?.getString("line2"), + addressBundle?.getString("postalCode"), + addressBundle?.getString("state")) + return PaymentSheet.BillingDetails( + address, + bundle.getString("email"), + bundle.getString("name"), + bundle.getString("phone")) + } + + internal fun createBillingDetailsCollectionConfiguration(bundle: Bundle): PaymentSheet.BillingDetailsCollectionConfiguration { + return PaymentSheet.BillingDetailsCollectionConfiguration( + name = mapToCollectionMode(bundle.getString("name")), + phone = mapToCollectionMode(bundle.getString("phone")), + email = mapToCollectionMode(bundle.getString("email")), + address = mapToAddressCollectionMode(bundle.getString("address")), + attachDefaultsToPaymentMethod = bundle.getBoolean("attachDefaultsToPaymentMethod") + ) + } + + internal fun createCustomerAdapter( + context: ReactApplicationContext, + customerId: String, + customerEphemeralKeySecret: String, + setupIntentClientSecret: String?, + customerAdapterOverrideParams: Bundle?, + ): ReactNativeCustomerAdapter { + val ephemeralKeyProvider = { + CustomerAdapter.Result.success( + CustomerEphemeralKey.create( + customerId = customerId, + ephemeralKey = customerEphemeralKeySecret, + ) + ) + } + val customerAdapter = if (setupIntentClientSecret != null) { + CustomerAdapter.create( + context, + customerEphemeralKeyProvider = ephemeralKeyProvider, + setupIntentClientSecretProvider = { + CustomerAdapter.Result.success( + setupIntentClientSecret, + ) + } + ) + } else { + CustomerAdapter.create( + context, + customerEphemeralKeyProvider = ephemeralKeyProvider, + setupIntentClientSecretProvider = null + ) + } + + return ReactNativeCustomerAdapter( + context = context, + adapter = customerAdapter, + overridesFetchPaymentMethods = customerAdapterOverrideParams?.getBoolean("fetchPaymentMethods") ?: false, + overridesAttachPaymentMethod = customerAdapterOverrideParams?.getBoolean("attachPaymentMethod") ?: false, + overridesDetachPaymentMethod = customerAdapterOverrideParams?.getBoolean("detachPaymentMethod") ?: false, + overridesSetSelectedPaymentOption = customerAdapterOverrideParams?.getBoolean("setSelectedPaymentOption") ?: false, + overridesFetchSelectedPaymentOption = customerAdapterOverrideParams?.getBoolean("fetchSelectedPaymentOption") ?: false, + overridesSetupIntentClientSecretForCustomerAttach = customerAdapterOverrideParams?.getBoolean("setupIntentClientSecretForCustomerAttach") ?: false + ) + } + + internal fun createPaymentOptionResult(selection: PaymentOptionSelection?): WritableMap { + var paymentOptionResult = Arguments.createMap() + + when (selection) { + is PaymentOptionSelection.GooglePay -> { + paymentOptionResult = buildResult( + selection.paymentOption.label, + selection.paymentOption.icon(), + null) + } + is PaymentOptionSelection.PaymentMethod -> { + paymentOptionResult = buildResult( + selection.paymentOption.label, + selection.paymentOption.icon(), + selection.paymentMethod) + } + null -> {} + } + + return paymentOptionResult + } + + private fun buildResult(label: String, drawable: Drawable, paymentMethod: PaymentMethod?): WritableMap { + val result = Arguments.createMap() + val paymentOption = Arguments.createMap().also { + it.putString("label", label) + it.putString("image", getBase64FromBitmap(getBitmapFromDrawable(drawable))) + } + result.putMap("paymentOption", paymentOption) + if (paymentMethod != null) { + result.putMap("paymentMethod", mapFromPaymentMethod(paymentMethod)) + } + return result + } + } +} diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/ReactNativeCustomerAdapter.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/ReactNativeCustomerAdapter.kt new file mode 100644 index 000000000..46260b6be --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/customersheet/ReactNativeCustomerAdapter.kt @@ -0,0 +1,138 @@ +package com.reactnativestripesdk.customersheet + +import android.util.Log +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.reactnativestripesdk.StripeSdkModule +import com.stripe.android.customersheet.CustomerAdapter +import com.stripe.android.customersheet.ExperimentalCustomerSheetApi +import com.stripe.android.model.PaymentMethod +import kotlinx.coroutines.CompletableDeferred + +@OptIn(ExperimentalCustomerSheetApi::class) +class ReactNativeCustomerAdapter ( + private val context: ReactApplicationContext, + private val adapter: CustomerAdapter, + private val overridesFetchPaymentMethods: Boolean, + private val overridesAttachPaymentMethod: Boolean, + private val overridesDetachPaymentMethod: Boolean, + private val overridesSetSelectedPaymentOption: Boolean, + private val overridesFetchSelectedPaymentOption: Boolean, + private val overridesSetupIntentClientSecretForCustomerAttach: Boolean +) : CustomerAdapter by adapter { + internal var fetchPaymentMethodsCallback: CompletableDeferred>? = null + internal var attachPaymentMethodCallback: CompletableDeferred? = null + internal var detachPaymentMethodCallback: CompletableDeferred? = null + internal var setSelectedPaymentOptionCallback: CompletableDeferred? = null + internal var fetchSelectedPaymentOptionCallback: CompletableDeferred? = null + internal var setupIntentClientSecretForCustomerAttachCallback: CompletableDeferred? = null + + override suspend fun retrievePaymentMethods(): CustomerAdapter.Result> { + if (overridesFetchPaymentMethods) { + CompletableDeferred>().also { + fetchPaymentMethodsCallback = it + emitEvent("onCustomerAdapterFetchPaymentMethodsCallback", Arguments.createMap()) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success(resultFromJavascript) + } + } + + return adapter.retrievePaymentMethods() + } + + override suspend fun attachPaymentMethod(paymentMethodId: String): CustomerAdapter.Result { + if (overridesAttachPaymentMethod) { + CompletableDeferred().also { + attachPaymentMethodCallback = it + val params = Arguments.createMap().also { + it.putString("paymentMethodId", paymentMethodId) + } + emitEvent("onCustomerAdapterAttachPaymentMethodCallback", params) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success(resultFromJavascript) + } + } + + return adapter.attachPaymentMethod(paymentMethodId) + } + + override suspend fun detachPaymentMethod(paymentMethodId: String): CustomerAdapter.Result { + if (overridesDetachPaymentMethod) { + CompletableDeferred().also { + detachPaymentMethodCallback = it + val params = Arguments.createMap().also { + it.putString("paymentMethodId", paymentMethodId) + } + emitEvent("onCustomerAdapterDetachPaymentMethodCallback", params) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success(resultFromJavascript) + } + } + + return adapter.detachPaymentMethod(paymentMethodId) + } + + override suspend fun setSelectedPaymentOption(paymentOption: CustomerAdapter.PaymentOption?): CustomerAdapter.Result { + if (overridesSetSelectedPaymentOption) { + CompletableDeferred().also { + setSelectedPaymentOptionCallback = it + val params = Arguments.createMap().also { + it.putString("paymentOption", paymentOption?.id) + } + emitEvent("onCustomerAdapterSetSelectedPaymentOptionCallback", params) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success(resultFromJavascript) + } + } + + return adapter.setSelectedPaymentOption(paymentOption) + } + + override suspend fun retrieveSelectedPaymentOption(): CustomerAdapter.Result { + if (overridesFetchSelectedPaymentOption) { + CompletableDeferred().also { + fetchSelectedPaymentOptionCallback = it + emitEvent("onCustomerAdapterFetchSelectedPaymentOptionCallback", Arguments.createMap()) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success( + if (resultFromJavascript != null) { + CustomerAdapter.PaymentOption.fromId(resultFromJavascript) + } else { + null + } + ) + } + } + + return adapter.retrieveSelectedPaymentOption() + } + + override suspend fun setupIntentClientSecretForCustomerAttach(): CustomerAdapter.Result { + if (overridesSetupIntentClientSecretForCustomerAttach) { + CompletableDeferred().also { + setupIntentClientSecretForCustomerAttachCallback = it + emitEvent("onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback", Arguments.createMap()) + val resultFromJavascript = it.await() + return CustomerAdapter.Result.success(resultFromJavascript) + } + } + + return adapter.setupIntentClientSecretForCustomerAttach() + } + + private fun emitEvent(eventName: String, params: WritableMap) { + val stripeSdkModule: StripeSdkModule? = context.getNativeModule(StripeSdkModule::class.java) + if (stripeSdkModule == null || stripeSdkModule.eventListenerCount == 0) { + Log.e( + "StripeReactNative", + "Tried to call $eventName, but no callback was found. Please file an issue: https://github.com/stripe/stripe-react-native/issues" + ) + } + + stripeSdkModule?.sendEvent(context, eventName, params) + } +} + + diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/utils/Mappers.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/utils/Mappers.kt index d6533c2e4..1bcb0314a 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/utils/Mappers.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/utils/Mappers.kt @@ -490,6 +490,18 @@ internal fun mapNextAction(type: NextActionType?, data: NextActionData?): Writab NextActionType.CashAppRedirect, NextActionType.BlikAuthorize, NextActionType.UseStripeSdk, NextActionType.UpiAwaitNotification, null -> { return null } + NextActionType.DisplayBoletoDetails -> { + (data as? NextActionData.DisplayBoletoDetails)?.let { + nextActionMap.putString("type", "boletoVoucher") + nextActionMap.putString("voucherURL", it.hostedVoucherUrl) + } + } + NextActionType.DisplayKonbiniDetails -> { + (data as? NextActionData.DisplayKonbiniDetails)?.let { + nextActionMap.putString("type", "konbiniVoucher") + nextActionMap.putString("voucherURL", it.hostedVoucherUrl) + } + } } return nextActionMap } @@ -807,11 +819,8 @@ internal fun mapFromSetupIntentResult(setupIntent: SetupIntent): WritableMap { map.putMap("lastSetupError", setupError) } - setupIntent.paymentMethodTypes.forEach { code -> - val type: PaymentMethod.Type? = PaymentMethod.Type.values().find { - code == it.code - } - type?.let { + for (code in setupIntent.paymentMethodTypes) { + PaymentMethod.Type.fromCode(code)?.let { paymentMethodTypes.pushString(mapPaymentMethodType(it)) } } @@ -881,3 +890,22 @@ fun toBundleObject(readableMap: ReadableMap?): Bundle { } return result } + +internal fun mapFromShippingContact(googlePayResult: GooglePayResult): WritableMap { + val map = WritableNativeMap() + map.putString("emailAddress", googlePayResult.email) + val name = WritableNativeMap() + googlePayResult.name + name.putString("givenName", googlePayResult.shippingInformation?.name) + map.putMap("name", name) + map.putString("phoneNumber", googlePayResult.phoneNumber) + val postalAddress = WritableNativeMap() + postalAddress.putString("city", googlePayResult.shippingInformation?.address?.city) + postalAddress.putString("country", googlePayResult.shippingInformation?.address?.country) + postalAddress.putString("postalCode", googlePayResult.shippingInformation?.address?.postalCode) + postalAddress.putString("state", googlePayResult.shippingInformation?.address?.state) + postalAddress.putString("street", googlePayResult.shippingInformation?.address?.line1 + "\n" + googlePayResult.shippingInformation?.address?.line2) + postalAddress.putString("isoCountryCode", googlePayResult.shippingInformation?.address?.country) + map.putMap("postalAddress", postalAddress) + return map +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift index bc5da251e..57e1e472b 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift @@ -21,9 +21,13 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC if let error = error { self.createPlatformPayPaymentMethodResolver?(Errors.createError(ErrorType.Failed, error)) } else { - let promiseResult = [ - "token": token != nil ? Mappers.mapFromToken(token: token!.splitApplePayAddressByNewline()) : [:] + var promiseResult = [ + "token": token != nil ? Mappers.mapFromToken(token: token!.splitApplePayAddressByNewline()) : [:], ] + if let shippingContact = payment.shippingContact { + promiseResult["shippingContact"] = Mappers.mapFromShippingContact(shippingContact: shippingContact) + } + self.createPlatformPayPaymentMethodResolver?(promiseResult) } completion(PKPaymentAuthorizationResult.init(status: .success, errors: nil)) @@ -33,9 +37,13 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC if let error = error { self.createPlatformPayPaymentMethodResolver?(Errors.createError(ErrorType.Failed, error)) } else { - let promiseResult = [ + var promiseResult = [ "paymentMethod": Mappers.mapFromPaymentMethod(paymentMethod?.splitApplePayAddressByNewline()) ?? [:] ] + if let shippingContact = payment.shippingContact { + promiseResult["shippingContact"] = Mappers.mapFromShippingContact(shippingContact: shippingContact) + } + self.createPlatformPayPaymentMethodResolver?(promiseResult) } completion(PKPaymentAuthorizationResult.init(status: .success, errors: nil)) @@ -157,6 +165,7 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC paymentInformation: PKPayment, completion: @escaping STPIntentClientSecretCompletionBlock ) { + self.confirmApplePayPaymentMethod = paymentMethod if let clientSecret = self.confirmApplePayPaymentClientSecret { completion(clientSecret, nil) } else if let clientSecret = self.confirmApplePaySetupClientSecret { @@ -199,10 +208,15 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC } if let paymentIntent = paymentIntent { - resolve(Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent))) + let result = Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent) + if (paymentIntent.paymentMethod == nil) { + result.setValue(Mappers.mapFromPaymentMethod(self.confirmApplePayPaymentMethod), forKey: "paymentMethod") + } + resolve(Mappers.createResult("paymentIntent", result)) } else { resolve(Mappers.createResult("paymentIntent", nil)) } + self.confirmApplePayPaymentMethod = nil } } else if let clientSecret = self.confirmApplePaySetupClientSecret { STPAPIClient.shared.retrieveSetupIntent(withClientSecret: clientSecret) { (setupIntent, error) in @@ -216,10 +230,15 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC } if let setupIntent = setupIntent { - resolve(Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: setupIntent))) + let result = Mappers.mapFromSetupIntent(setupIntent: setupIntent) + if (setupIntent.paymentMethod == nil) { + result.setValue(Mappers.mapFromPaymentMethod(self.confirmApplePayPaymentMethod), forKey: "paymentMethod") + } + resolve(Mappers.createResult("setupIntent", result)) } else { resolve(Mappers.createResult("setupIntent", nil)) } + self.confirmApplePayPaymentMethod = nil } } } diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift new file mode 100644 index 000000000..3b71ebc33 --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift @@ -0,0 +1,181 @@ +// +// CustomerSheetUtils.swift +// stripe-react-native +// +// Created by Charles Cruzan on 08/28/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) import StripePaymentSheet + +class CustomerSheetUtils { + internal class func buildCustomerSheetConfiguration( + appearance: PaymentSheet.Appearance, + style: PaymentSheet.UserInterfaceStyle, + removeSavedPaymentMethodMessage: String?, + returnURL: String?, + headerTextForSelectionScreen: String?, + applePayEnabled: Bool?, + merchantDisplayName: String?, + billingDetailsCollectionConfiguration: NSDictionary?, + defaultBillingDetails: NSDictionary? + ) -> CustomerSheet.Configuration { + var config = CustomerSheet.Configuration() + config.appearance = appearance + config.style = style + config.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage + config.returnURL = returnURL + config.headerTextForSelectionScreen = headerTextForSelectionScreen + config.applePayEnabled = applePayEnabled ?? false + if let merchantDisplayName = merchantDisplayName { + config.merchantDisplayName = merchantDisplayName + } + if let billingConfigParams = billingDetailsCollectionConfiguration { + config.billingDetailsCollectionConfiguration.name = StripeSdk.mapToCollectionMode(str: billingConfigParams["name"] as? String) + config.billingDetailsCollectionConfiguration.phone = StripeSdk.mapToCollectionMode(str: billingConfigParams["phone"] as? String) + config.billingDetailsCollectionConfiguration.email = StripeSdk.mapToCollectionMode(str: billingConfigParams["email"] as? String) + config.billingDetailsCollectionConfiguration.address = StripeSdk.mapToAddressCollectionMode(str: billingConfigParams["address"] as? String) + config.billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod = billingConfigParams["attachDefaultsToPaymentMethod"] as? Bool == true + } + if let defaultBillingDetails = defaultBillingDetails { + config.defaultBillingDetails.name = defaultBillingDetails["name"] as? String + config.defaultBillingDetails.email = defaultBillingDetails["email"] as? String + config.defaultBillingDetails.phone = defaultBillingDetails["phone"] as? String + if let address = defaultBillingDetails["address"] as? [String: String] { + config.defaultBillingDetails.address = .init(city: address["city"], + country: address["country"], + line1: address["line1"], + line2: address["line2"], + postalCode: address["postalCode"], + state: address["state"]) + } + } + return config + } + + internal class func buildStripeCustomerAdapter( + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + customerAdapter: NSDictionary, + stripeSdk: StripeSdk + ) -> StripeCustomerAdapter { + if (customerAdapter.count > 0) { + return buildCustomerAdapterOverride( + customerAdapter: customerAdapter, + customerId: customerId, + ephemeralKeySecret: ephemeralKeySecret, + setupIntentClientSecret: setupIntentClientSecret, + stripeSdk: stripeSdk + ) + } + + if let setupIntentClientSecret = setupIntentClientSecret { + return StripeCustomerAdapter( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + }, + setupIntentClientSecretProvider: { + return setupIntentClientSecret + } + ) + } + + return StripeCustomerAdapter( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + } + ) + } + + internal class func buildCustomerAdapterOverride( + customerAdapter: NSDictionary, + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + stripeSdk: StripeSdk + ) -> StripeCustomerAdapter { + return ReactNativeCustomerAdapter( + fetchPaymentMethods: customerAdapter["fetchPaymentMethods"] as? Bool ?? false, + attachPaymentMethod: customerAdapter["attachPaymentMethod"] as? Bool ?? false, + detachPaymentMethod: customerAdapter["detachPaymentMethod"] as? Bool ?? false, + setSelectedPaymentOption: customerAdapter["setSelectedPaymentOption"] as? Bool ?? false, + fetchSelectedPaymentOption: customerAdapter["fetchSelectedPaymentOption"] as? Bool ?? false, + setupIntentClientSecretForCustomerAttach: customerAdapter["setupIntentClientSecretForCustomerAttach"] as? Bool ?? false, + customerId: customerId, + ephemeralKeySecret: ephemeralKeySecret, + setupIntentClientSecret: setupIntentClientSecret, + stripeSdk: stripeSdk + ) + } + + internal class func getModalPresentationStyle(_ string: String?) -> UIModalPresentationStyle { + switch (string) { + case "fullscreen": + return .fullScreen + case "popover": + fallthrough + default: + return .popover + } + } + + internal class func getModalTransitionStyle(_ string: String?) -> UIModalTransitionStyle { + switch (string) { + case "flip": + return .flipHorizontal + case "curl": + return .partialCurl + case "dissolve": + return .crossDissolve + case "slide": + fallthrough + default: + return .coverVertical + } + } + + + internal class func buildPaymentOptionResult(label: String, imageData: String?, paymentMethod: STPPaymentMethod?) -> NSMutableDictionary { + let result: NSMutableDictionary = [:] + let paymentOption: NSMutableDictionary = [:] + paymentOption.setValue(label, forKey: "label") + if (imageData != nil) { + paymentOption.setValue(imageData, forKey: "image") + } + result.setValue(paymentOption, forKey: "paymentOption") + if (paymentMethod != nil) { + result.setValue(Mappers.mapFromPaymentMethod(paymentMethod), forKey: "paymentMethod") + } + return result + } + + internal class func interpretResult(result: CustomerSheet.CustomerSheetResult) -> NSDictionary { + var payload: NSMutableDictionary = [:] + switch result { + case .error(let error): + return Errors.createError(ErrorType.Failed, error as NSError) + case .selected(let paymentOption): + switch paymentOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + case .canceled(let paymentOption): + switch paymentOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + payload.setValue(["code": ErrorType.Canceled], forKey: "error") + } + return payload + } + +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift new file mode 100644 index 000000000..c1ccf81f4 --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift @@ -0,0 +1,173 @@ +// +// ReactNativeCustomerAdapter.swift +// stripe-react-native +// +// Created by Charlie Cruzan on 9/5/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) @_spi(STP) import StripePaymentSheet + +class ReactNativeCustomerAdapter: StripeCustomerAdapter { + var overridesFetchPaymentMethods: Bool + var overridesAttachPaymentMethod: Bool + var overridesDetachPaymentMethod: Bool + var overridesSetSelectedPaymentOption: Bool + var overridesFetchSelectedPaymentOption: Bool + var overridesSetupIntentClientSecretForCustomerAttach: Bool + var stripeSdk: StripeSdk + + init( + fetchPaymentMethods: Bool, + attachPaymentMethod: Bool, + detachPaymentMethod: Bool, + setSelectedPaymentOption: Bool, + fetchSelectedPaymentOption: Bool, + setupIntentClientSecretForCustomerAttach: Bool, + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + stripeSdk: StripeSdk + ) { + self.overridesFetchPaymentMethods = fetchPaymentMethods + self.overridesAttachPaymentMethod = attachPaymentMethod + self.overridesDetachPaymentMethod = detachPaymentMethod + self.overridesSetSelectedPaymentOption = setSelectedPaymentOption + self.overridesFetchSelectedPaymentOption = fetchSelectedPaymentOption + self.overridesSetupIntentClientSecretForCustomerAttach = setupIntentClientSecretForCustomerAttach + self.stripeSdk = stripeSdk + + if let setupIntentClientSecret = setupIntentClientSecret { + super.init( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + }, + setupIntentClientSecretProvider: { + return setupIntentClientSecret + } + ) + } else { + super.init( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + } + ) + } + } + + override func fetchPaymentMethods() async throws -> [STPPaymentMethod] { + if (self.overridesFetchPaymentMethods) { + return await withCheckedContinuation({ continuation in + fetchPaymentMethods { paymentMethods in + continuation.resume(returning: paymentMethods) + } + }) + } else { + return try await super.fetchPaymentMethods() + } + } + + override func attachPaymentMethod(_ paymentMethodId: String) async throws { + if (self.overridesAttachPaymentMethod) { + return await withCheckedContinuation({ continuation in + attachPaymentMethod(paymentMethodId) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.attachPaymentMethod(paymentMethodId) + } + } + + override func detachPaymentMethod(paymentMethodId: String) async throws { + if (self.overridesDetachPaymentMethod) { + return await withCheckedContinuation({ continuation in + detachPaymentMethod(paymentMethodId) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.detachPaymentMethod(paymentMethodId: paymentMethodId) + } + } + + override func setSelectedPaymentOption(paymentOption: CustomerPaymentOption?) async throws { + if (self.overridesSetSelectedPaymentOption) { + return await withCheckedContinuation({ continuation in + setSelectedPaymentOption(paymentOption) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.setSelectedPaymentOption(paymentOption: paymentOption) + } + } + + override func fetchSelectedPaymentOption() async throws -> CustomerPaymentOption? { + if (self.overridesFetchSelectedPaymentOption) { + return await withCheckedContinuation({ continuation in + fetchSelectedPaymentOption { paymentOption in + continuation.resume(returning: paymentOption) + } + }) + } else { + return try await super.fetchSelectedPaymentOption() + } + } + + override func setupIntentClientSecretForCustomerAttach() async throws -> String { + if (self.overridesSetupIntentClientSecretForCustomerAttach) { + return await withCheckedContinuation({ continuation in + setupIntentClientSecretForCustomerAttach { clientSecret in + continuation.resume(returning: clientSecret) + } + }) + } else { + return try await super.setupIntentClientSecretForCustomerAttach() + } + } +} + +extension ReactNativeCustomerAdapter { + func fetchPaymentMethods(completion: @escaping ([STPPaymentMethod]) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.fetchPaymentMethodsCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterFetchPaymentMethodsCallback", body: [:]) + } + } + + func attachPaymentMethod(_ paymentMethodId: String, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.attachPaymentMethodCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterAttachPaymentMethodCallback", body: ["paymentMethodId": paymentMethodId]) + } + } + + func detachPaymentMethod(_ paymentMethodId: String, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.detachPaymentMethodCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterDetachPaymentMethodCallback", body: ["paymentMethodId": paymentMethodId]) + } + } + + func setSelectedPaymentOption(_ paymentOption: CustomerPaymentOption?, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.setSelectedPaymentOptionCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterSetSelectedPaymentOptionCallback", body: ["paymentOption": paymentOption?.value]) + } + } + + func fetchSelectedPaymentOption(completion: @escaping (CustomerPaymentOption?) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.fetchSelectedPaymentOptionCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterFetchSelectedPaymentOptionCallback", body: [:]) + } + } + + func setupIntentClientSecretForCustomerAttach(completion: @escaping (String) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.setupIntentClientSecretForCustomerAttachCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback", body: [:]) + } + } +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift index fc1dba808..88880cab4 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift @@ -451,14 +451,16 @@ class Mappers { "voucherURL": it.oxxoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), "voucherNumber": it.oxxoDisplayDetails?.number ?? NSNull(), ] -// TODO: Not supported on Android -// case .boletoDisplayDetails: -// return [ -// "type": "boletoVoucher", -// "expiration": it.boletoDisplayDetails?.expiresAt.timeIntervalSince1970.description ?? NSNull(), -// "voucherURL": it.boletoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), -// "voucherNumber": it.boletoDisplayDetails?.number ?? NSNull(), -// ] + case .boletoDisplayDetails: + return [ + "type": "boletoVoucher", + "voucherURL": it.boletoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), + ] + case .konbiniDisplayDetails: + return [ + "type": "konbiniVoucher", + "voucherURL": it.konbiniDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), + ] default: // .useStripeSDK, .BLIKAuthorize, .unknown return nil } @@ -754,7 +756,7 @@ class Mappers { } @available(iOS 13.0, *) - class func mapToUserInterfaceStyle(_ style: String) -> PaymentSheet.UserInterfaceStyle { + class func mapToUserInterfaceStyle(_ style: String?) -> PaymentSheet.UserInterfaceStyle { switch style { case "alwaysDark": return PaymentSheet.UserInterfaceStyle.alwaysDark case "alwaysLight": return PaymentSheet.UserInterfaceStyle.alwaysLight diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift new file mode 100644 index 000000000..2afc2d1ff --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift @@ -0,0 +1,166 @@ +// +// CustomerSheetView.swift +// stripe-react-native +// +// Created by Charles Cruzan on 08/28/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) @_spi(STP) import StripePaymentSheet + +extension StripeSdk { + @objc(initCustomerSheet:customerAdapterOverrides:resolver:rejecter:) + func initCustomerSheet(params: NSDictionary, customerAdapterOverrides: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + do { + customerSheetConfiguration = CustomerSheetUtils.buildCustomerSheetConfiguration( + appearance: try PaymentSheetAppearance.buildAppearanceFromParams(userParams: params["appearance"] as? NSDictionary), + style: Mappers.mapToUserInterfaceStyle(params["style"] as? String), + removeSavedPaymentMethodMessage: params["removeSavedPaymentMethodMessage"] as? String, + returnURL: params["returnURL"] as? String, + headerTextForSelectionScreen: params["headerTextForSelectionScreen"] as? String, + applePayEnabled: params["applePayEnabled"] as? Bool, + merchantDisplayName: params["merchantDisplayName"] as? String, + billingDetailsCollectionConfiguration: params["billingDetailsCollectionConfiguration"] as? NSDictionary, + defaultBillingDetails: params["defaultBillingDetails"] as? NSDictionary) + } catch { + resolve( + Errors.createError(ErrorType.Failed, error.localizedDescription) + ) + return + } + + guard let customerId = params["customerId"] as? String else { + resolve(Errors.createError(ErrorType.Failed, "You must provide `customerId`")) + return + } + guard let customerEphemeralKeySecret = params["customerEphemeralKeySecret"] as? String else { + resolve(Errors.createError(ErrorType.Failed, "You must provide `customerEphemeralKeySecret`")) + return + } + + customerAdapter = CustomerSheetUtils.buildStripeCustomerAdapter( + customerId: customerId, + ephemeralKeySecret: customerEphemeralKeySecret, + setupIntentClientSecret: params["setupIntentClientSecret"] as? String, + customerAdapter: customerAdapterOverrides, + stripeSdk: self + ) + + customerSheet = CustomerSheet(configuration: customerSheetConfiguration, customer: customerAdapter!) + + resolve([]) + } + + @objc(presentCustomerSheet:resolver:rejecter:) + func presentCustomerSheet(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + if (STPAPIClient.shared.publishableKey == nil) { + resolve( + Errors.createError(ErrorType.Failed, "No publishable key set. Stripe has not been initialized. Initialize Stripe in your app with the StripeProvider component or the initStripe method.") + ) + return + } + + if let timeout = params["timeout"] as? Double { + DispatchQueue.main.asyncAfter(deadline: .now() + timeout/1000) { + if let customerSheetViewController = self.customerSheetViewController { + customerSheetViewController.dismiss(animated: true) + resolve(Errors.createError(ErrorType.Timeout, "The payment has timed out.")) + } + } + } + + DispatchQueue.main.async { + self.customerSheetViewController = findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + if let customerSheetViewController = self.customerSheetViewController { + customerSheetViewController.modalPresentationStyle = CustomerSheetUtils.getModalPresentationStyle(params["presentationStyle"] as? String) + customerSheetViewController.modalTransitionStyle = CustomerSheetUtils.getModalTransitionStyle(params["animationStyle"] as? String) + if let customerSheet = self.customerSheet { + customerSheet.present( + from: customerSheetViewController, completion: { result in + resolve(CustomerSheetUtils.interpretResult(result: result)) + }) + } else { + resolve(Errors.createError(ErrorType.Failed, "CustomerSheet has not been properly initialized.")) + return + } + } + } + } + + @objc(retrieveCustomerSheetPaymentOptionSelection:rejecter:) + func retrieveCustomerSheetPaymentOptionSelection(resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + guard let customerAdapter = customerAdapter else { + resolve(Errors.createError(ErrorType.Failed, "CustomerSheet has not been properly initialized.")) + return + } + + Task { + var payload: NSDictionary = [:] + var paymentMethodOption: CustomerSheet.PaymentOptionSelection? = nil + do { + paymentMethodOption = try await customerAdapter.retrievePaymentOptionSelection() + } catch { + resolve(Errors.createError(ErrorType.Failed, error as NSError)) + return + } + + switch paymentMethodOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + resolve(payload) + } + } + + @objc(customerAdapterFetchPaymentMethodsCallback:resolver:rejecter:) + func customerAdapterFetchPaymentMethodsCallback(paymentMethods: [NSDictionary], resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + let decodedPaymentMethods = paymentMethods.compactMap { STPPaymentMethod.decodedObject(fromAPIResponse: $0 as? [AnyHashable : Any]) } + self.fetchPaymentMethodsCallback?(decodedPaymentMethods) + resolve([]) + } + + @objc(customerAdapterAttachPaymentMethodCallback:resolver:rejecter:) + func customerAdapterAttachPaymentMethodCallback(unusedPaymentMethod: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.attachPaymentMethodCallback?() + resolve([]) + } + + @objc(customerAdapterDetachPaymentMethodCallback:resolver:rejecter:) + func customerAdapterDetachPaymentMethodCallback(unusedPaymentMethod: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.detachPaymentMethodCallback?() + resolve([]) + } + + @objc(customerAdapterSetSelectedPaymentOptionCallback:rejecter:) + func customerAdapterSetSelectedPaymentOptionCallback(resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.setSelectedPaymentOptionCallback?() + resolve([]) + } + + @objc(customerAdapterFetchSelectedPaymentOptionCallback:resolver:rejecter:) + func customerAdapterFetchSelectedPaymentOptionCallback(paymentOption: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + if let paymentOption = paymentOption { + self.fetchSelectedPaymentOptionCallback?(CustomerPaymentOption.init(value: paymentOption)) + } else { + self.fetchSelectedPaymentOptionCallback?(nil) + } + resolve([]) + } + + @objc(customerAdapterSetupIntentClientSecretForCustomerAttachCallback:resolver:rejecter:) + func customerAdapterSetupIntentClientSecretForCustomerAttachCallback(clientSecret: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.setupIntentClientSecretForCustomerAttachCallback?(clientSecret) + resolve([]) + } +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift index 13b70f550..b7dd3b1ff 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift @@ -50,6 +50,10 @@ extension StripeSdk { configuration.allowsDelayedPaymentMethods = allowsDelayedPaymentMethods } + if let removeSavedPaymentMethodMessage = params["removeSavedPaymentMethodMessage"] as? String { + configuration.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage + } + if let billingConfigParams = params["billingDetailsCollectionConfiguration"] as? [String: Any?] { configuration.billingDetailsCollectionConfiguration.name = StripeSdk.mapToCollectionMode(str: billingConfigParams["name"] as? String) configuration.billingDetailsCollectionConfiguration.phone = StripeSdk.mapToCollectionMode(str: billingConfigParams["phone"] as? String) @@ -262,7 +266,7 @@ extension StripeSdk { }) } - private static func mapToCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode { + internal static func mapToCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode { switch str { case "automatic": return .automatic @@ -275,7 +279,7 @@ extension StripeSdk { } } - private static func mapToAddressCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode { + internal static func mapToAddressCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode { switch str { case "automatic": return .automatic diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift index c5254aff0..6c8390680 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift @@ -22,6 +22,7 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap var confirmApplePayResolver: RCTPromiseResolveBlock? = nil var confirmApplePayPaymentClientSecret: String? = nil var confirmApplePaySetupClientSecret: String? = nil + var confirmApplePayPaymentMethod: STPPaymentMethod? = nil var applePaymentAuthorizationController: PKPaymentAuthorizationViewController? = nil var createPlatformPayPaymentMethodResolver: RCTPromiseResolveBlock? = nil @@ -48,6 +49,17 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap var applePayShippingAddressErrors: [Error]? = nil var applePayCouponCodeErrors: [Error]? = nil + var customerSheetConfiguration = CustomerSheet.Configuration() + var customerSheet: CustomerSheet? = nil + var customerAdapter: StripeCustomerAdapter? = nil + var customerSheetViewController: UIViewController? + var fetchPaymentMethodsCallback: (([STPPaymentMethod]) -> Void)? = nil + var attachPaymentMethodCallback: (() -> Void)? = nil + var detachPaymentMethodCallback: (() -> Void)? = nil + var setSelectedPaymentOptionCallback: (() -> Void)? = nil + var fetchSelectedPaymentOptionCallback: ((CustomerPaymentOption?) -> Void)? = nil + var setupIntentClientSecretForCustomerAttachCallback: ((String) -> Void)? = nil + var hasEventListeners = false override func startObserving() { hasEventListeners = true @@ -57,7 +69,9 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap } override func supportedEvents() -> [String]! { - return ["onOrderTrackingCallback", "onConfirmHandlerCallback"] + return ["onOrderTrackingCallback", "onConfirmHandlerCallback", "onCustomerAdapterFetchPaymentMethodsCallback", "onCustomerAdapterAttachPaymentMethodCallback", + "onCustomerAdapterDetachPaymentMethodCallback", "onCustomerAdapterSetSelectedPaymentOptionCallback", "onCustomerAdapterFetchSelectedPaymentOptionCallback", + "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback"] } @objc override static func requiresMainQueueSetup() -> Bool { @@ -252,20 +266,20 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap if (paymentMethodType == .USBankAccount && paymentMethodData == nil) { return STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret, paymentMethodType: .USBankAccount) } else { + let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) let parameters = STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret) if let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String { parameters.paymentMethodID = paymentMethodId } else { - let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) do { - let paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) - parameters.paymentMethodParams = paymentMethodParams - parameters.mandateData = factory.createMandateData() + parameters.paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) } catch { err = Errors.createError(ErrorType.Failed, error as NSError?) } } + + parameters.mandateData = factory.createMandateData() return parameters } @@ -829,7 +843,6 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap paymentMethodData: NSDictionary?, options: NSDictionary ) -> (NSDictionary?, STPPaymentIntentParams) { - let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) var err: NSDictionary? = nil let paymentIntentParams: STPPaymentIntentParams = { @@ -838,7 +851,7 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret, paymentMethodType: .USBankAccount) } else { guard let paymentMethodType = paymentMethodType else { return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) } - + let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String let parameters = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) @@ -846,15 +859,19 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap parameters.paymentMethodId = paymentMethodId } else { do { - let paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) - let paymentMethodOptions = try factory.createOptions(paymentMethodType: paymentMethodType) - parameters.paymentMethodParams = paymentMethodParams - parameters.paymentMethodOptions = paymentMethodOptions - parameters.mandateData = factory.createMandateData() + parameters.paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) } catch { err = Errors.createError(ErrorType.Failed, error as NSError?) } } + + do { + parameters.paymentMethodOptions = try factory.createOptions(paymentMethodType: paymentMethodType) + parameters.mandateData = factory.createMandateData() + } catch { + err = Errors.createError(ErrorType.Failed, error as NSError?) + } + return parameters } }() diff --git a/packages/stripe_ios/ios/Classes/StripePlugin.swift b/packages/stripe_ios/ios/Classes/StripePlugin.swift index 1615e40a8..30052c3e2 100644 --- a/packages/stripe_ios/ios/Classes/StripePlugin.swift +++ b/packages/stripe_ios/ios/Classes/StripePlugin.swift @@ -117,6 +117,93 @@ class StripePlugin: StripeSdk, FlutterPlugin, ViewManagerDelegate { return removeListener(call, result: result) case "intentCreationCallback": return intentCreationCallback(call, result: result) + case "initCustomerSheet": + guard let arguments = call.arguments as? FlutterMap, + let customerAdapterOverrides = arguments["customerAdapterOverrides"] as? NSDictionary, + let params = arguments["params"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return initCustomerSheet( + params: params, + customerAdapterOverrides: customerAdapterOverrides, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "presentCustomerSheet": + guard let arguments = call.arguments as? FlutterMap, + let params = arguments["params"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return presentCustomerSheet( + params: params, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "retrieveCustomerSheetPaymentOptionSelection": + return retrieveCustomerSheetPaymentOptionSelection( + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterFetchPaymentMethodsCallback": + guard let arguments = call.arguments as? FlutterMap, + let paymentMethods = arguments["paymentMethods"] as? [NSDictionary] else { + result(FlutterError.invalidParams) + return + } + return customerAdapterFetchPaymentMethodsCallback( + paymentMethods: paymentMethods, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterAttachPaymentMethodCallback": + guard let arguments = call.arguments as? FlutterMap, + let unusedPaymentMethod = arguments["unusedPaymentMethod"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return customerAdapterAttachPaymentMethodCallback( + unusedPaymentMethod: unusedPaymentMethod, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterDetachPaymentMethodCallback": + guard let arguments = call.arguments as? FlutterMap, + let unusedPaymentMethod = arguments["unusedPaymentMethod"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return customerAdapterDetachPaymentMethodCallback( + unusedPaymentMethod: unusedPaymentMethod, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterSetSelectedPaymentOptionCallback": + return customerAdapterSetSelectedPaymentOptionCallback( + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterFetchSelectedPaymentOptionCallback": + let arguments = call.arguments as? FlutterMap + let paymentOption = arguments?["paymentOption"] as? String + + return customerAdapterFetchSelectedPaymentOptionCallback( + paymentOption: paymentOption, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterSetupIntentClientSecretForCustomerAttachCallback": + guard let arguments = call.arguments as? FlutterMap, + let clientSecret = arguments["clientSecret"] as? String else { + result(FlutterError.invalidParams) + return + } + return customerAdapterSetupIntentClientSecretForCustomerAttachCallback( + clientSecret: clientSecret, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) default: result(FlutterMethodNotImplemented) } diff --git a/packages/stripe_ios/ios/stripe_ios.podspec b/packages/stripe_ios/ios/stripe_ios.podspec index 389c92ecf..dff2f77ad 100644 --- a/packages/stripe_ios/ios/stripe_ios.podspec +++ b/packages/stripe_ios/ios/stripe_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint stripe_ios.podspec' to validate before publishing. # -stripe_version = '~> 23.12.0' +stripe_version = '~> 23.16.0' Pod::Spec.new do |s| s.name = 'stripe_ios' s.version = '0.0.1' diff --git a/packages/stripe_js/test/src/js/tokens/create_token_test.dart b/packages/stripe_js/test/src/js/tokens/create_token_test.dart index 0725986d8..064de08f3 100644 --- a/packages/stripe_js/test/src/js/tokens/create_token_test.dart +++ b/packages/stripe_js/test/src/js/tokens/create_token_test.dart @@ -97,6 +97,7 @@ void main() { response.error?.toJson(), equals({ 'type': 'invalid_request_error', + 'code': 'bank_account_unusable', 'param': 'bank_account[country]', 'message': 'Country INVALID not supported (you should use the 2-letter country code, e.g. US).' 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 42141813d..417c1052b 100644 --- a/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart +++ b/packages/stripe_platform_interface/lib/src/method_channel_stripe.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:stripe_platform_interface/src/models/ach_params.dart'; import 'package:stripe_platform_interface/src/models/create_token_data.dart'; +import 'package:stripe_platform_interface/src/models/customer_sheet.dart'; import 'package:stripe_platform_interface/src/models/financial_connections.dart'; import 'package:stripe_platform_interface/src/models/google_pay.dart'; import 'package:stripe_platform_interface/src/models/intent_creation_callback_params.dart'; @@ -246,6 +247,47 @@ class MethodChannelStripe extends StripePlatform { } } + @override + Future initCustomerSheet( + CustomerSheetInitParams params) async { + final result = await _methodChannel.invokeMethod( + 'initCustomerSheet', + { + 'params': params.toJson(), + 'customerAdapterOverrides': {}, + }, + ); + + if (result is List) { + return null; + } else { + return _parseCustomerSheetResult(result); + } + } + + @override + Future presentCustomerSheet({ + CustomerSheetPresentParams? options, + }) async { + final result = await _methodChannel.invokeMethod( + 'presentCustomerSheet', + {'params': {}, 'options': options?.toJson() ?? {}}, + ); + + return _parseCustomerSheetResult(result); + } + + @override + Future + retrieveCustomerSheetPaymentOptionSelection() async { + final result = await _methodChannel.invokeMethod( + 'retrieveCustomerSheetPaymentOptionSelection', + {}, + ); + + return _parseCustomerSheetResult(result); + } + @override Future createToken(CreateTokenParams params) async { final invokeParams = params.map( @@ -305,6 +347,33 @@ class MethodChannelStripe extends StripePlatform { } } + CustomerSheetResult? _parseCustomerSheetResult(Map? result) { + if (result != null) { + if (result.isEmpty) { + return null; + } else if (result['paymentOption'] != null) { + return CustomerSheetResult.fromJson(result); + } else { + if (result['error'] != null) { + //workaround for tojson in sumtypes + result['runtimeType'] = 'failed'; + throw StripeException.fromJson(result); + } else { + throw StripeError( + message: + 'Unknown result this is likely a problem in the plugin $result', + code: CustomerSheetError.unknown, + ); + } + } + } else { + throw const StripeError( + message: 'Result should not be null', + code: CustomerSheetError.unknown, + ); + } + } + @override Future createGooglePayPaymentMethod( CreateGooglePayPaymentParams params) async { diff --git a/packages/stripe_platform_interface/lib/src/models/customer_sheet.dart b/packages/stripe_platform_interface/lib/src/models/customer_sheet.dart new file mode 100644 index 000000000..079240a45 --- /dev/null +++ b/packages/stripe_platform_interface/lib/src/models/customer_sheet.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stripe_platform_interface/stripe_platform_interface.dart'; + +part 'customer_sheet.freezed.dart'; +part 'customer_sheet.g.dart'; + +/// Parameters to setup a Customer sheet +/// +/// For more info see https://stripe.com/docs/elements/customer-sheet?platform=react-native +@freezed +class CustomerSheetInitParams with _$CustomerSheetInitParams { + @JsonSerializable(explicitToJson: true) + const factory CustomerSheetInitParams({ + /// Color styling used for the Customersheet UI + @JsonKey(toJson: UserInterfaceStyleKey.toJson) ThemeMode? style, + + /// Appearance of the customersheet. + /// + /// When no appearance defined it will fallback to [style] or Stripe default. + PaymentSheetAppearance? appearance, + + /// Optional but recommended for cards, required for other payment methods. The SetupIntent client secret that will be used to confirm a new payment method. If this is missing, you will only be able to add cards without authentication steps. + String? setupIntentClientSecret, + + /// The identifier of the Stripe Customer object. See https://stripe.com/docs/api/customers/object#customer_object-id + required String customerId, + + /// A short-lived token that allows the SDK to access a Customer's payment methods. + required String customerEphemeralKeySecret, + + /// Your customer-facing business name. The default value is the name of your app. + String? merchantDisplayName, + + /// Optional configuration for setting the header text of the Payment Method selection screen + String? headerTextForSelectionScreen, + + /// CustomerSheet pre-populates fields with the values provided. If `billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod` is `true`, these values will be attached to the payment method even if they are not collected by the CustomerSheet UI. + BillingDetails? defaultBillingDetails, + + /// Describes how billing details should be collected. All values default to `AUTOMATIC`. If `NEVER` is used for a required field for the Payment Method, you must provide an appropriate value as part of `defaultBillingDetails`. + BillingDetailsCollectionConfiguration? + billingDetailsCollectionConfiguration, + + /// URL that redirects back to your app that CustomerSheet can use to auto-dismiss web views used for additional authentication, e.g. 3DS2 + String? returnURL, + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? removeSavedPaymentMethodMessage, + + /// Whether to show Apple Pay as an option. Defaults to `false`. + @Default(true) bool applePayEnabled, + + /// Whether to show Google Pay as an option. Defaults to `false`. + @Default(true) bool googlePayEnabled, + }) = _CustomerSheetInitParams; + + factory CustomerSheetInitParams.fromJson(Map json) => + _$CustomerSheetInitParamsFromJson(json); +} + +@freezed +class CustomerSheetPresentParams with _$CustomerSheetPresentParams { + @JsonSerializable(explicitToJson: true) + const factory CustomerSheetPresentParams({ + /// Controls how the modal is presented (after animation). iOS only. Defaults to `popover`. + CustomerSheetPresentationStyle? presentationStyle, + + /// Controls how the modal animates. iOS only. + CustomerSheetAnimationStyle? animationStyle, + + /// Time (in milliseconds) before the Customer Sheet will automatically dismiss. + int? timeout, + }) = _CustomerSheetPresentParams; + + factory CustomerSheetPresentParams.fromJson(Map json) => + _$CustomerSheetPresentParamsFromJson(json); +} + +@freezed +class CustomerSheetResult with _$CustomerSheetResult { + @JsonSerializable(explicitToJson: true) + const factory CustomerSheetResult({ + /// The users selected payment option, if one exists. + PaymentSheetPaymentOption? paymentOption, + + /// The Stripe PaymentMethod associated with the paymentOption, if it exists. + PaymentMethod? paymentMethod, + + /// The error that occurred + StripeError? error, + }) = _CustomerSheetResult; + + factory CustomerSheetResult.fromJson(Map json) => + _$CustomerSheetResultFromJson(json); +} + +enum CustomerSheetAnimationStyle { + flip, + curl, + slide, + dissolve, +} + +enum CustomerSheetPresentationStyle { + fullscreen, + popover, +} + +/* + /** Optional override. It is generally recommended to rely on the default behavior, but- provide a CustomerAdapter here if + * you would prefer retrieving and updating your Stripe customer object via your own backend instead. + * WARNING: When implementing your own CustomerAdapter, ensure your application complies with all applicable laws and regulations, including data privacy and consumer protection. + */ + customerAdapter?: CustomerAdapter; +*/ diff --git a/packages/stripe_platform_interface/lib/src/models/customer_sheet.freezed.dart b/packages/stripe_platform_interface/lib/src/models/customer_sheet.freezed.dart new file mode 100644 index 000000000..ff0bdc468 --- /dev/null +++ b/packages/stripe_platform_interface/lib/src/models/customer_sheet.freezed.dart @@ -0,0 +1,1044 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'customer_sheet.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +CustomerSheetInitParams _$CustomerSheetInitParamsFromJson( + Map json) { + return _CustomerSheetInitParams.fromJson(json); +} + +/// @nodoc +mixin _$CustomerSheetInitParams { + /// Color styling used for the Customersheet UI + @JsonKey(toJson: UserInterfaceStyleKey.toJson) + ThemeMode? get style => throw _privateConstructorUsedError; + + /// Appearance of the customersheet. + /// + /// When no appearance defined it will fallback to [style] or Stripe default. + PaymentSheetAppearance? get appearance => throw _privateConstructorUsedError; + + /// Optional but recommended for cards, required for other payment methods. The SetupIntent client secret that will be used to confirm a new payment method. If this is missing, you will only be able to add cards without authentication steps. + String? get setupIntentClientSecret => throw _privateConstructorUsedError; + + /// The identifier of the Stripe Customer object. See https://stripe.com/docs/api/customers/object#customer_object-id + String get customerId => throw _privateConstructorUsedError; + + /// A short-lived token that allows the SDK to access a Customer's payment methods. + String get customerEphemeralKeySecret => throw _privateConstructorUsedError; + + /// Your customer-facing business name. The default value is the name of your app. + String? get merchantDisplayName => throw _privateConstructorUsedError; + + /// Optional configuration for setting the header text of the Payment Method selection screen + String? get headerTextForSelectionScreen => + throw _privateConstructorUsedError; + + /// CustomerSheet pre-populates fields with the values provided. If `billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod` is `true`, these values will be attached to the payment method even if they are not collected by the CustomerSheet UI. + BillingDetails? get defaultBillingDetails => + throw _privateConstructorUsedError; + + /// Describes how billing details should be collected. All values default to `AUTOMATIC`. If `NEVER` is used for a required field for the Payment Method, you must provide an appropriate value as part of `defaultBillingDetails`. + BillingDetailsCollectionConfiguration? + get billingDetailsCollectionConfiguration => + throw _privateConstructorUsedError; + + /// URL that redirects back to your app that CustomerSheet can use to auto-dismiss web views used for additional authentication, e.g. 3DS2 + String? get returnURL => throw _privateConstructorUsedError; + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? get removeSavedPaymentMethodMessage => + throw _privateConstructorUsedError; + + /// Whether to show Apple Pay as an option. Defaults to `false`. + bool get applePayEnabled => throw _privateConstructorUsedError; + + /// Whether to show Google Pay as an option. Defaults to `false`. + bool get googlePayEnabled => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CustomerSheetInitParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerSheetInitParamsCopyWith<$Res> { + factory $CustomerSheetInitParamsCopyWith(CustomerSheetInitParams value, + $Res Function(CustomerSheetInitParams) then) = + _$CustomerSheetInitParamsCopyWithImpl<$Res, CustomerSheetInitParams>; + @useResult + $Res call( + {@JsonKey(toJson: UserInterfaceStyleKey.toJson) ThemeMode? style, + PaymentSheetAppearance? appearance, + String? setupIntentClientSecret, + String customerId, + String customerEphemeralKeySecret, + String? merchantDisplayName, + String? headerTextForSelectionScreen, + BillingDetails? defaultBillingDetails, + BillingDetailsCollectionConfiguration? + billingDetailsCollectionConfiguration, + String? returnURL, + String? removeSavedPaymentMethodMessage, + bool applePayEnabled, + bool googlePayEnabled}); + + $PaymentSheetAppearanceCopyWith<$Res>? get appearance; + $BillingDetailsCopyWith<$Res>? get defaultBillingDetails; + $BillingDetailsCollectionConfigurationCopyWith<$Res>? + get billingDetailsCollectionConfiguration; +} + +/// @nodoc +class _$CustomerSheetInitParamsCopyWithImpl<$Res, + $Val extends CustomerSheetInitParams> + implements $CustomerSheetInitParamsCopyWith<$Res> { + _$CustomerSheetInitParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? style = freezed, + Object? appearance = freezed, + Object? setupIntentClientSecret = freezed, + Object? customerId = null, + Object? customerEphemeralKeySecret = null, + Object? merchantDisplayName = freezed, + Object? headerTextForSelectionScreen = freezed, + Object? defaultBillingDetails = freezed, + Object? billingDetailsCollectionConfiguration = freezed, + Object? returnURL = freezed, + Object? removeSavedPaymentMethodMessage = freezed, + Object? applePayEnabled = null, + Object? googlePayEnabled = null, + }) { + return _then(_value.copyWith( + style: freezed == style + ? _value.style + : style // ignore: cast_nullable_to_non_nullable + as ThemeMode?, + appearance: freezed == appearance + ? _value.appearance + : appearance // ignore: cast_nullable_to_non_nullable + as PaymentSheetAppearance?, + setupIntentClientSecret: freezed == setupIntentClientSecret + ? _value.setupIntentClientSecret + : setupIntentClientSecret // ignore: cast_nullable_to_non_nullable + as String?, + customerId: null == customerId + ? _value.customerId + : customerId // ignore: cast_nullable_to_non_nullable + as String, + customerEphemeralKeySecret: null == customerEphemeralKeySecret + ? _value.customerEphemeralKeySecret + : customerEphemeralKeySecret // ignore: cast_nullable_to_non_nullable + as String, + merchantDisplayName: freezed == merchantDisplayName + ? _value.merchantDisplayName + : merchantDisplayName // ignore: cast_nullable_to_non_nullable + as String?, + headerTextForSelectionScreen: freezed == headerTextForSelectionScreen + ? _value.headerTextForSelectionScreen + : headerTextForSelectionScreen // ignore: cast_nullable_to_non_nullable + as String?, + defaultBillingDetails: freezed == defaultBillingDetails + ? _value.defaultBillingDetails + : defaultBillingDetails // ignore: cast_nullable_to_non_nullable + as BillingDetails?, + billingDetailsCollectionConfiguration: freezed == + billingDetailsCollectionConfiguration + ? _value.billingDetailsCollectionConfiguration + : billingDetailsCollectionConfiguration // ignore: cast_nullable_to_non_nullable + as BillingDetailsCollectionConfiguration?, + returnURL: freezed == returnURL + ? _value.returnURL + : returnURL // ignore: cast_nullable_to_non_nullable + as String?, + removeSavedPaymentMethodMessage: freezed == + removeSavedPaymentMethodMessage + ? _value.removeSavedPaymentMethodMessage + : removeSavedPaymentMethodMessage // ignore: cast_nullable_to_non_nullable + as String?, + applePayEnabled: null == applePayEnabled + ? _value.applePayEnabled + : applePayEnabled // ignore: cast_nullable_to_non_nullable + as bool, + googlePayEnabled: null == googlePayEnabled + ? _value.googlePayEnabled + : googlePayEnabled // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $PaymentSheetAppearanceCopyWith<$Res>? get appearance { + if (_value.appearance == null) { + return null; + } + + return $PaymentSheetAppearanceCopyWith<$Res>(_value.appearance!, (value) { + return _then(_value.copyWith(appearance: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $BillingDetailsCopyWith<$Res>? get defaultBillingDetails { + if (_value.defaultBillingDetails == null) { + return null; + } + + return $BillingDetailsCopyWith<$Res>(_value.defaultBillingDetails!, + (value) { + return _then(_value.copyWith(defaultBillingDetails: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $BillingDetailsCollectionConfigurationCopyWith<$Res>? + get billingDetailsCollectionConfiguration { + if (_value.billingDetailsCollectionConfiguration == null) { + return null; + } + + return $BillingDetailsCollectionConfigurationCopyWith<$Res>( + _value.billingDetailsCollectionConfiguration!, (value) { + return _then(_value.copyWith(billingDetailsCollectionConfiguration: value) + as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_CustomerSheetInitParamsCopyWith<$Res> + implements $CustomerSheetInitParamsCopyWith<$Res> { + factory _$$_CustomerSheetInitParamsCopyWith(_$_CustomerSheetInitParams value, + $Res Function(_$_CustomerSheetInitParams) then) = + __$$_CustomerSheetInitParamsCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(toJson: UserInterfaceStyleKey.toJson) ThemeMode? style, + PaymentSheetAppearance? appearance, + String? setupIntentClientSecret, + String customerId, + String customerEphemeralKeySecret, + String? merchantDisplayName, + String? headerTextForSelectionScreen, + BillingDetails? defaultBillingDetails, + BillingDetailsCollectionConfiguration? + billingDetailsCollectionConfiguration, + String? returnURL, + String? removeSavedPaymentMethodMessage, + bool applePayEnabled, + bool googlePayEnabled}); + + @override + $PaymentSheetAppearanceCopyWith<$Res>? get appearance; + @override + $BillingDetailsCopyWith<$Res>? get defaultBillingDetails; + @override + $BillingDetailsCollectionConfigurationCopyWith<$Res>? + get billingDetailsCollectionConfiguration; +} + +/// @nodoc +class __$$_CustomerSheetInitParamsCopyWithImpl<$Res> + extends _$CustomerSheetInitParamsCopyWithImpl<$Res, + _$_CustomerSheetInitParams> + implements _$$_CustomerSheetInitParamsCopyWith<$Res> { + __$$_CustomerSheetInitParamsCopyWithImpl(_$_CustomerSheetInitParams _value, + $Res Function(_$_CustomerSheetInitParams) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? style = freezed, + Object? appearance = freezed, + Object? setupIntentClientSecret = freezed, + Object? customerId = null, + Object? customerEphemeralKeySecret = null, + Object? merchantDisplayName = freezed, + Object? headerTextForSelectionScreen = freezed, + Object? defaultBillingDetails = freezed, + Object? billingDetailsCollectionConfiguration = freezed, + Object? returnURL = freezed, + Object? removeSavedPaymentMethodMessage = freezed, + Object? applePayEnabled = null, + Object? googlePayEnabled = null, + }) { + return _then(_$_CustomerSheetInitParams( + style: freezed == style + ? _value.style + : style // ignore: cast_nullable_to_non_nullable + as ThemeMode?, + appearance: freezed == appearance + ? _value.appearance + : appearance // ignore: cast_nullable_to_non_nullable + as PaymentSheetAppearance?, + setupIntentClientSecret: freezed == setupIntentClientSecret + ? _value.setupIntentClientSecret + : setupIntentClientSecret // ignore: cast_nullable_to_non_nullable + as String?, + customerId: null == customerId + ? _value.customerId + : customerId // ignore: cast_nullable_to_non_nullable + as String, + customerEphemeralKeySecret: null == customerEphemeralKeySecret + ? _value.customerEphemeralKeySecret + : customerEphemeralKeySecret // ignore: cast_nullable_to_non_nullable + as String, + merchantDisplayName: freezed == merchantDisplayName + ? _value.merchantDisplayName + : merchantDisplayName // ignore: cast_nullable_to_non_nullable + as String?, + headerTextForSelectionScreen: freezed == headerTextForSelectionScreen + ? _value.headerTextForSelectionScreen + : headerTextForSelectionScreen // ignore: cast_nullable_to_non_nullable + as String?, + defaultBillingDetails: freezed == defaultBillingDetails + ? _value.defaultBillingDetails + : defaultBillingDetails // ignore: cast_nullable_to_non_nullable + as BillingDetails?, + billingDetailsCollectionConfiguration: freezed == + billingDetailsCollectionConfiguration + ? _value.billingDetailsCollectionConfiguration + : billingDetailsCollectionConfiguration // ignore: cast_nullable_to_non_nullable + as BillingDetailsCollectionConfiguration?, + returnURL: freezed == returnURL + ? _value.returnURL + : returnURL // ignore: cast_nullable_to_non_nullable + as String?, + removeSavedPaymentMethodMessage: freezed == + removeSavedPaymentMethodMessage + ? _value.removeSavedPaymentMethodMessage + : removeSavedPaymentMethodMessage // ignore: cast_nullable_to_non_nullable + as String?, + applePayEnabled: null == applePayEnabled + ? _value.applePayEnabled + : applePayEnabled // ignore: cast_nullable_to_non_nullable + as bool, + googlePayEnabled: null == googlePayEnabled + ? _value.googlePayEnabled + : googlePayEnabled // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true) +class _$_CustomerSheetInitParams implements _CustomerSheetInitParams { + const _$_CustomerSheetInitParams( + {@JsonKey(toJson: UserInterfaceStyleKey.toJson) this.style, + this.appearance, + this.setupIntentClientSecret, + required this.customerId, + required this.customerEphemeralKeySecret, + this.merchantDisplayName, + this.headerTextForSelectionScreen, + this.defaultBillingDetails, + this.billingDetailsCollectionConfiguration, + this.returnURL, + this.removeSavedPaymentMethodMessage, + this.applePayEnabled = true, + this.googlePayEnabled = true}); + + factory _$_CustomerSheetInitParams.fromJson(Map json) => + _$$_CustomerSheetInitParamsFromJson(json); + + /// Color styling used for the Customersheet UI + @override + @JsonKey(toJson: UserInterfaceStyleKey.toJson) + final ThemeMode? style; + + /// Appearance of the customersheet. + /// + /// When no appearance defined it will fallback to [style] or Stripe default. + @override + final PaymentSheetAppearance? appearance; + + /// Optional but recommended for cards, required for other payment methods. The SetupIntent client secret that will be used to confirm a new payment method. If this is missing, you will only be able to add cards without authentication steps. + @override + final String? setupIntentClientSecret; + + /// The identifier of the Stripe Customer object. See https://stripe.com/docs/api/customers/object#customer_object-id + @override + final String customerId; + + /// A short-lived token that allows the SDK to access a Customer's payment methods. + @override + final String customerEphemeralKeySecret; + + /// Your customer-facing business name. The default value is the name of your app. + @override + final String? merchantDisplayName; + + /// Optional configuration for setting the header text of the Payment Method selection screen + @override + final String? headerTextForSelectionScreen; + + /// CustomerSheet pre-populates fields with the values provided. If `billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod` is `true`, these values will be attached to the payment method even if they are not collected by the CustomerSheet UI. + @override + final BillingDetails? defaultBillingDetails; + + /// Describes how billing details should be collected. All values default to `AUTOMATIC`. If `NEVER` is used for a required field for the Payment Method, you must provide an appropriate value as part of `defaultBillingDetails`. + @override + final BillingDetailsCollectionConfiguration? + billingDetailsCollectionConfiguration; + + /// URL that redirects back to your app that CustomerSheet can use to auto-dismiss web views used for additional authentication, e.g. 3DS2 + @override + final String? returnURL; + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + @override + final String? removeSavedPaymentMethodMessage; + + /// Whether to show Apple Pay as an option. Defaults to `false`. + @override + @JsonKey() + final bool applePayEnabled; + + /// Whether to show Google Pay as an option. Defaults to `false`. + @override + @JsonKey() + final bool googlePayEnabled; + + @override + String toString() { + return 'CustomerSheetInitParams(style: $style, appearance: $appearance, setupIntentClientSecret: $setupIntentClientSecret, customerId: $customerId, customerEphemeralKeySecret: $customerEphemeralKeySecret, merchantDisplayName: $merchantDisplayName, headerTextForSelectionScreen: $headerTextForSelectionScreen, defaultBillingDetails: $defaultBillingDetails, billingDetailsCollectionConfiguration: $billingDetailsCollectionConfiguration, returnURL: $returnURL, removeSavedPaymentMethodMessage: $removeSavedPaymentMethodMessage, applePayEnabled: $applePayEnabled, googlePayEnabled: $googlePayEnabled)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CustomerSheetInitParams && + (identical(other.style, style) || other.style == style) && + (identical(other.appearance, appearance) || + other.appearance == appearance) && + (identical(other.setupIntentClientSecret, setupIntentClientSecret) || + other.setupIntentClientSecret == setupIntentClientSecret) && + (identical(other.customerId, customerId) || + other.customerId == customerId) && + (identical(other.customerEphemeralKeySecret, customerEphemeralKeySecret) || + other.customerEphemeralKeySecret == + customerEphemeralKeySecret) && + (identical(other.merchantDisplayName, merchantDisplayName) || + other.merchantDisplayName == merchantDisplayName) && + (identical(other.headerTextForSelectionScreen, + headerTextForSelectionScreen) || + other.headerTextForSelectionScreen == + headerTextForSelectionScreen) && + (identical(other.defaultBillingDetails, defaultBillingDetails) || + other.defaultBillingDetails == defaultBillingDetails) && + (identical(other.billingDetailsCollectionConfiguration, + billingDetailsCollectionConfiguration) || + other.billingDetailsCollectionConfiguration == + billingDetailsCollectionConfiguration) && + (identical(other.returnURL, returnURL) || + other.returnURL == returnURL) && + (identical(other.removeSavedPaymentMethodMessage, + removeSavedPaymentMethodMessage) || + other.removeSavedPaymentMethodMessage == + removeSavedPaymentMethodMessage) && + (identical(other.applePayEnabled, applePayEnabled) || + other.applePayEnabled == applePayEnabled) && + (identical(other.googlePayEnabled, googlePayEnabled) || + other.googlePayEnabled == googlePayEnabled)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + style, + appearance, + setupIntentClientSecret, + customerId, + customerEphemeralKeySecret, + merchantDisplayName, + headerTextForSelectionScreen, + defaultBillingDetails, + billingDetailsCollectionConfiguration, + returnURL, + removeSavedPaymentMethodMessage, + applePayEnabled, + googlePayEnabled); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CustomerSheetInitParamsCopyWith<_$_CustomerSheetInitParams> + get copyWith => + __$$_CustomerSheetInitParamsCopyWithImpl<_$_CustomerSheetInitParams>( + this, _$identity); + + @override + Map toJson() { + return _$$_CustomerSheetInitParamsToJson( + this, + ); + } +} + +abstract class _CustomerSheetInitParams implements CustomerSheetInitParams { + const factory _CustomerSheetInitParams( + {@JsonKey(toJson: UserInterfaceStyleKey.toJson) final ThemeMode? style, + final PaymentSheetAppearance? appearance, + final String? setupIntentClientSecret, + required final String customerId, + required final String customerEphemeralKeySecret, + final String? merchantDisplayName, + final String? headerTextForSelectionScreen, + final BillingDetails? defaultBillingDetails, + final BillingDetailsCollectionConfiguration? + billingDetailsCollectionConfiguration, + final String? returnURL, + final String? removeSavedPaymentMethodMessage, + final bool applePayEnabled, + final bool googlePayEnabled}) = _$_CustomerSheetInitParams; + + factory _CustomerSheetInitParams.fromJson(Map json) = + _$_CustomerSheetInitParams.fromJson; + + @override + + /// Color styling used for the Customersheet UI + @JsonKey(toJson: UserInterfaceStyleKey.toJson) + ThemeMode? get style; + @override + + /// Appearance of the customersheet. + /// + /// When no appearance defined it will fallback to [style] or Stripe default. + PaymentSheetAppearance? get appearance; + @override + + /// Optional but recommended for cards, required for other payment methods. The SetupIntent client secret that will be used to confirm a new payment method. If this is missing, you will only be able to add cards without authentication steps. + String? get setupIntentClientSecret; + @override + + /// The identifier of the Stripe Customer object. See https://stripe.com/docs/api/customers/object#customer_object-id + String get customerId; + @override + + /// A short-lived token that allows the SDK to access a Customer's payment methods. + String get customerEphemeralKeySecret; + @override + + /// Your customer-facing business name. The default value is the name of your app. + String? get merchantDisplayName; + @override + + /// Optional configuration for setting the header text of the Payment Method selection screen + String? get headerTextForSelectionScreen; + @override + + /// CustomerSheet pre-populates fields with the values provided. If `billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod` is `true`, these values will be attached to the payment method even if they are not collected by the CustomerSheet UI. + BillingDetails? get defaultBillingDetails; + @override + + /// Describes how billing details should be collected. All values default to `AUTOMATIC`. If `NEVER` is used for a required field for the Payment Method, you must provide an appropriate value as part of `defaultBillingDetails`. + BillingDetailsCollectionConfiguration? + get billingDetailsCollectionConfiguration; + @override + + /// URL that redirects back to your app that CustomerSheet can use to auto-dismiss web views used for additional authentication, e.g. 3DS2 + String? get returnURL; + @override + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? get removeSavedPaymentMethodMessage; + @override + + /// Whether to show Apple Pay as an option. Defaults to `false`. + bool get applePayEnabled; + @override + + /// Whether to show Google Pay as an option. Defaults to `false`. + bool get googlePayEnabled; + @override + @JsonKey(ignore: true) + _$$_CustomerSheetInitParamsCopyWith<_$_CustomerSheetInitParams> + get copyWith => throw _privateConstructorUsedError; +} + +CustomerSheetPresentParams _$CustomerSheetPresentParamsFromJson( + Map json) { + return _CustomerSheetPresentParams.fromJson(json); +} + +/// @nodoc +mixin _$CustomerSheetPresentParams { + /// Controls how the modal is presented (after animation). iOS only. Defaults to `popover`. + CustomerSheetPresentationStyle? get presentationStyle => + throw _privateConstructorUsedError; + + /// Controls how the modal animates. iOS only. + CustomerSheetAnimationStyle? get animationStyle => + throw _privateConstructorUsedError; + + /// Time (in milliseconds) before the Customer Sheet will automatically dismiss. + int? get timeout => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CustomerSheetPresentParamsCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerSheetPresentParamsCopyWith<$Res> { + factory $CustomerSheetPresentParamsCopyWith(CustomerSheetPresentParams value, + $Res Function(CustomerSheetPresentParams) then) = + _$CustomerSheetPresentParamsCopyWithImpl<$Res, + CustomerSheetPresentParams>; + @useResult + $Res call( + {CustomerSheetPresentationStyle? presentationStyle, + CustomerSheetAnimationStyle? animationStyle, + int? timeout}); +} + +/// @nodoc +class _$CustomerSheetPresentParamsCopyWithImpl<$Res, + $Val extends CustomerSheetPresentParams> + implements $CustomerSheetPresentParamsCopyWith<$Res> { + _$CustomerSheetPresentParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presentationStyle = freezed, + Object? animationStyle = freezed, + Object? timeout = freezed, + }) { + return _then(_value.copyWith( + presentationStyle: freezed == presentationStyle + ? _value.presentationStyle + : presentationStyle // ignore: cast_nullable_to_non_nullable + as CustomerSheetPresentationStyle?, + animationStyle: freezed == animationStyle + ? _value.animationStyle + : animationStyle // ignore: cast_nullable_to_non_nullable + as CustomerSheetAnimationStyle?, + timeout: freezed == timeout + ? _value.timeout + : timeout // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CustomerSheetPresentParamsCopyWith<$Res> + implements $CustomerSheetPresentParamsCopyWith<$Res> { + factory _$$_CustomerSheetPresentParamsCopyWith( + _$_CustomerSheetPresentParams value, + $Res Function(_$_CustomerSheetPresentParams) then) = + __$$_CustomerSheetPresentParamsCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {CustomerSheetPresentationStyle? presentationStyle, + CustomerSheetAnimationStyle? animationStyle, + int? timeout}); +} + +/// @nodoc +class __$$_CustomerSheetPresentParamsCopyWithImpl<$Res> + extends _$CustomerSheetPresentParamsCopyWithImpl<$Res, + _$_CustomerSheetPresentParams> + implements _$$_CustomerSheetPresentParamsCopyWith<$Res> { + __$$_CustomerSheetPresentParamsCopyWithImpl( + _$_CustomerSheetPresentParams _value, + $Res Function(_$_CustomerSheetPresentParams) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? presentationStyle = freezed, + Object? animationStyle = freezed, + Object? timeout = freezed, + }) { + return _then(_$_CustomerSheetPresentParams( + presentationStyle: freezed == presentationStyle + ? _value.presentationStyle + : presentationStyle // ignore: cast_nullable_to_non_nullable + as CustomerSheetPresentationStyle?, + animationStyle: freezed == animationStyle + ? _value.animationStyle + : animationStyle // ignore: cast_nullable_to_non_nullable + as CustomerSheetAnimationStyle?, + timeout: freezed == timeout + ? _value.timeout + : timeout // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true) +class _$_CustomerSheetPresentParams implements _CustomerSheetPresentParams { + const _$_CustomerSheetPresentParams( + {this.presentationStyle, this.animationStyle, this.timeout}); + + factory _$_CustomerSheetPresentParams.fromJson(Map json) => + _$$_CustomerSheetPresentParamsFromJson(json); + + /// Controls how the modal is presented (after animation). iOS only. Defaults to `popover`. + @override + final CustomerSheetPresentationStyle? presentationStyle; + + /// Controls how the modal animates. iOS only. + @override + final CustomerSheetAnimationStyle? animationStyle; + + /// Time (in milliseconds) before the Customer Sheet will automatically dismiss. + @override + final int? timeout; + + @override + String toString() { + return 'CustomerSheetPresentParams(presentationStyle: $presentationStyle, animationStyle: $animationStyle, timeout: $timeout)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CustomerSheetPresentParams && + (identical(other.presentationStyle, presentationStyle) || + other.presentationStyle == presentationStyle) && + (identical(other.animationStyle, animationStyle) || + other.animationStyle == animationStyle) && + (identical(other.timeout, timeout) || other.timeout == timeout)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, presentationStyle, animationStyle, timeout); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CustomerSheetPresentParamsCopyWith<_$_CustomerSheetPresentParams> + get copyWith => __$$_CustomerSheetPresentParamsCopyWithImpl< + _$_CustomerSheetPresentParams>(this, _$identity); + + @override + Map toJson() { + return _$$_CustomerSheetPresentParamsToJson( + this, + ); + } +} + +abstract class _CustomerSheetPresentParams + implements CustomerSheetPresentParams { + const factory _CustomerSheetPresentParams( + {final CustomerSheetPresentationStyle? presentationStyle, + final CustomerSheetAnimationStyle? animationStyle, + final int? timeout}) = _$_CustomerSheetPresentParams; + + factory _CustomerSheetPresentParams.fromJson(Map json) = + _$_CustomerSheetPresentParams.fromJson; + + @override + + /// Controls how the modal is presented (after animation). iOS only. Defaults to `popover`. + CustomerSheetPresentationStyle? get presentationStyle; + @override + + /// Controls how the modal animates. iOS only. + CustomerSheetAnimationStyle? get animationStyle; + @override + + /// Time (in milliseconds) before the Customer Sheet will automatically dismiss. + int? get timeout; + @override + @JsonKey(ignore: true) + _$$_CustomerSheetPresentParamsCopyWith<_$_CustomerSheetPresentParams> + get copyWith => throw _privateConstructorUsedError; +} + +CustomerSheetResult _$CustomerSheetResultFromJson(Map json) { + return _CustomerSheetResult.fromJson(json); +} + +/// @nodoc +mixin _$CustomerSheetResult { + /// The users selected payment option, if one exists. + PaymentSheetPaymentOption? get paymentOption => + throw _privateConstructorUsedError; + + /// The Stripe PaymentMethod associated with the paymentOption, if it exists. + PaymentMethod? get paymentMethod => throw _privateConstructorUsedError; + + /// The error that occurred + StripeError? get error => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CustomerSheetResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CustomerSheetResultCopyWith<$Res> { + factory $CustomerSheetResultCopyWith( + CustomerSheetResult value, $Res Function(CustomerSheetResult) then) = + _$CustomerSheetResultCopyWithImpl<$Res, CustomerSheetResult>; + @useResult + $Res call( + {PaymentSheetPaymentOption? paymentOption, + PaymentMethod? paymentMethod, + StripeError? error}); + + $PaymentSheetPaymentOptionCopyWith<$Res>? get paymentOption; + $PaymentMethodCopyWith<$Res>? get paymentMethod; + $StripeErrorCopyWith? get error; +} + +/// @nodoc +class _$CustomerSheetResultCopyWithImpl<$Res, $Val extends CustomerSheetResult> + implements $CustomerSheetResultCopyWith<$Res> { + _$CustomerSheetResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? paymentOption = freezed, + Object? paymentMethod = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + paymentOption: freezed == paymentOption + ? _value.paymentOption + : paymentOption // ignore: cast_nullable_to_non_nullable + as PaymentSheetPaymentOption?, + paymentMethod: freezed == paymentMethod + ? _value.paymentMethod + : paymentMethod // ignore: cast_nullable_to_non_nullable + as PaymentMethod?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StripeError?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $PaymentSheetPaymentOptionCopyWith<$Res>? get paymentOption { + if (_value.paymentOption == null) { + return null; + } + + return $PaymentSheetPaymentOptionCopyWith<$Res>(_value.paymentOption!, + (value) { + return _then(_value.copyWith(paymentOption: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PaymentMethodCopyWith<$Res>? get paymentMethod { + if (_value.paymentMethod == null) { + return null; + } + + return $PaymentMethodCopyWith<$Res>(_value.paymentMethod!, (value) { + return _then(_value.copyWith(paymentMethod: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $StripeErrorCopyWith? get error { + if (_value.error == null) { + return null; + } + + return $StripeErrorCopyWith(_value.error!, (value) { + return _then(_value.copyWith(error: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_CustomerSheetResultCopyWith<$Res> + implements $CustomerSheetResultCopyWith<$Res> { + factory _$$_CustomerSheetResultCopyWith(_$_CustomerSheetResult value, + $Res Function(_$_CustomerSheetResult) then) = + __$$_CustomerSheetResultCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {PaymentSheetPaymentOption? paymentOption, + PaymentMethod? paymentMethod, + StripeError? error}); + + @override + $PaymentSheetPaymentOptionCopyWith<$Res>? get paymentOption; + @override + $PaymentMethodCopyWith<$Res>? get paymentMethod; + @override + $StripeErrorCopyWith? get error; +} + +/// @nodoc +class __$$_CustomerSheetResultCopyWithImpl<$Res> + extends _$CustomerSheetResultCopyWithImpl<$Res, _$_CustomerSheetResult> + implements _$$_CustomerSheetResultCopyWith<$Res> { + __$$_CustomerSheetResultCopyWithImpl(_$_CustomerSheetResult _value, + $Res Function(_$_CustomerSheetResult) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? paymentOption = freezed, + Object? paymentMethod = freezed, + Object? error = freezed, + }) { + return _then(_$_CustomerSheetResult( + paymentOption: freezed == paymentOption + ? _value.paymentOption + : paymentOption // ignore: cast_nullable_to_non_nullable + as PaymentSheetPaymentOption?, + paymentMethod: freezed == paymentMethod + ? _value.paymentMethod + : paymentMethod // ignore: cast_nullable_to_non_nullable + as PaymentMethod?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StripeError?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true) +class _$_CustomerSheetResult implements _CustomerSheetResult { + const _$_CustomerSheetResult( + {this.paymentOption, this.paymentMethod, this.error}); + + factory _$_CustomerSheetResult.fromJson(Map json) => + _$$_CustomerSheetResultFromJson(json); + + /// The users selected payment option, if one exists. + @override + final PaymentSheetPaymentOption? paymentOption; + + /// The Stripe PaymentMethod associated with the paymentOption, if it exists. + @override + final PaymentMethod? paymentMethod; + + /// The error that occurred + @override + final StripeError? error; + + @override + String toString() { + return 'CustomerSheetResult(paymentOption: $paymentOption, paymentMethod: $paymentMethod, error: $error)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_CustomerSheetResult && + (identical(other.paymentOption, paymentOption) || + other.paymentOption == paymentOption) && + (identical(other.paymentMethod, paymentMethod) || + other.paymentMethod == paymentMethod) && + (identical(other.error, error) || other.error == error)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, paymentOption, paymentMethod, error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CustomerSheetResultCopyWith<_$_CustomerSheetResult> get copyWith => + __$$_CustomerSheetResultCopyWithImpl<_$_CustomerSheetResult>( + this, _$identity); + + @override + Map toJson() { + return _$$_CustomerSheetResultToJson( + this, + ); + } +} + +abstract class _CustomerSheetResult implements CustomerSheetResult { + const factory _CustomerSheetResult( + {final PaymentSheetPaymentOption? paymentOption, + final PaymentMethod? paymentMethod, + final StripeError? error}) = _$_CustomerSheetResult; + + factory _CustomerSheetResult.fromJson(Map json) = + _$_CustomerSheetResult.fromJson; + + @override + + /// The users selected payment option, if one exists. + PaymentSheetPaymentOption? get paymentOption; + @override + + /// The Stripe PaymentMethod associated with the paymentOption, if it exists. + PaymentMethod? get paymentMethod; + @override + + /// The error that occurred + StripeError? get error; + @override + @JsonKey(ignore: true) + _$$_CustomerSheetResultCopyWith<_$_CustomerSheetResult> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/stripe_platform_interface/lib/src/models/customer_sheet.g.dart b/packages/stripe_platform_interface/lib/src/models/customer_sheet.g.dart new file mode 100644 index 000000000..5ce67f87f --- /dev/null +++ b/packages/stripe_platform_interface/lib/src/models/customer_sheet.g.dart @@ -0,0 +1,121 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'customer_sheet.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_CustomerSheetInitParams _$$_CustomerSheetInitParamsFromJson( + Map json) => + _$_CustomerSheetInitParams( + style: $enumDecodeNullable(_$ThemeModeEnumMap, json['style']), + appearance: json['appearance'] == null + ? null + : PaymentSheetAppearance.fromJson( + json['appearance'] as Map), + setupIntentClientSecret: json['setupIntentClientSecret'] as String?, + customerId: json['customerId'] as String, + customerEphemeralKeySecret: json['customerEphemeralKeySecret'] as String, + merchantDisplayName: json['merchantDisplayName'] as String?, + headerTextForSelectionScreen: + json['headerTextForSelectionScreen'] as String?, + defaultBillingDetails: json['defaultBillingDetails'] == null + ? null + : BillingDetails.fromJson( + json['defaultBillingDetails'] as Map), + billingDetailsCollectionConfiguration: + json['billingDetailsCollectionConfiguration'] == null + ? null + : BillingDetailsCollectionConfiguration.fromJson( + json['billingDetailsCollectionConfiguration'] + as Map), + returnURL: json['returnURL'] as String?, + removeSavedPaymentMethodMessage: + json['removeSavedPaymentMethodMessage'] as String?, + applePayEnabled: json['applePayEnabled'] as bool? ?? true, + googlePayEnabled: json['googlePayEnabled'] as bool? ?? true, + ); + +Map _$$_CustomerSheetInitParamsToJson( + _$_CustomerSheetInitParams instance) => + { + 'style': UserInterfaceStyleKey.toJson(instance.style), + 'appearance': instance.appearance?.toJson(), + 'setupIntentClientSecret': instance.setupIntentClientSecret, + 'customerId': instance.customerId, + 'customerEphemeralKeySecret': instance.customerEphemeralKeySecret, + 'merchantDisplayName': instance.merchantDisplayName, + 'headerTextForSelectionScreen': instance.headerTextForSelectionScreen, + 'defaultBillingDetails': instance.defaultBillingDetails?.toJson(), + 'billingDetailsCollectionConfiguration': + instance.billingDetailsCollectionConfiguration?.toJson(), + 'returnURL': instance.returnURL, + 'removeSavedPaymentMethodMessage': + instance.removeSavedPaymentMethodMessage, + 'applePayEnabled': instance.applePayEnabled, + 'googlePayEnabled': instance.googlePayEnabled, + }; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +_$_CustomerSheetPresentParams _$$_CustomerSheetPresentParamsFromJson( + Map json) => + _$_CustomerSheetPresentParams( + presentationStyle: $enumDecodeNullable( + _$CustomerSheetPresentationStyleEnumMap, json['presentationStyle']), + animationStyle: $enumDecodeNullable( + _$CustomerSheetAnimationStyleEnumMap, json['animationStyle']), + timeout: json['timeout'] as int?, + ); + +Map _$$_CustomerSheetPresentParamsToJson( + _$_CustomerSheetPresentParams instance) => + { + 'presentationStyle': + _$CustomerSheetPresentationStyleEnumMap[instance.presentationStyle], + 'animationStyle': + _$CustomerSheetAnimationStyleEnumMap[instance.animationStyle], + 'timeout': instance.timeout, + }; + +const _$CustomerSheetPresentationStyleEnumMap = { + CustomerSheetPresentationStyle.fullscreen: 'fullscreen', + CustomerSheetPresentationStyle.popover: 'popover', +}; + +const _$CustomerSheetAnimationStyleEnumMap = { + CustomerSheetAnimationStyle.flip: 'flip', + CustomerSheetAnimationStyle.curl: 'curl', + CustomerSheetAnimationStyle.slide: 'slide', + CustomerSheetAnimationStyle.dissolve: 'dissolve', +}; + +_$_CustomerSheetResult _$$_CustomerSheetResultFromJson( + Map json) => + _$_CustomerSheetResult( + paymentOption: json['paymentOption'] == null + ? null + : PaymentSheetPaymentOption.fromJson( + json['paymentOption'] as Map), + paymentMethod: json['paymentMethod'] == null + ? null + : PaymentMethod.fromJson( + json['paymentMethod'] as Map), + error: json['error'] == null + ? null + : StripeError.fromJson( + json['error'] as Map), + ); + +Map _$$_CustomerSheetResultToJson( + _$_CustomerSheetResult instance) => + { + 'paymentOption': instance.paymentOption?.toJson(), + 'paymentMethod': instance.paymentMethod?.toJson(), + 'error': instance.error?.toJson(), + }; diff --git a/packages/stripe_platform_interface/lib/src/models/errors.dart b/packages/stripe_platform_interface/lib/src/models/errors.dart index 1589ef7a2..fdb1dccad 100644 --- a/packages/stripe_platform_interface/lib/src/models/errors.dart +++ b/packages/stripe_platform_interface/lib/src/models/errors.dart @@ -10,6 +10,8 @@ enum CreateTokenError { unknown } enum PaymentSheetError { unknown } +enum CustomerSheetError { unknown, failed, canceled } + @freezed /// Wrapper class that represents an error with the Stripe platform. diff --git a/packages/stripe_platform_interface/lib/src/models/google_pay.dart b/packages/stripe_platform_interface/lib/src/models/google_pay.dart index 365bdcd49..d97cca217 100644 --- a/packages/stripe_platform_interface/lib/src/models/google_pay.dart +++ b/packages/stripe_platform_interface/lib/src/models/google_pay.dart @@ -52,6 +52,12 @@ class GooglePayInitParams with _$GooglePayInitParams { /// When `true` Google Pay is considered ready if the customers's Google Pay /// wallet has existing payment methods. @Default(true) bool existingPaymentMethodRequired, + + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? label, + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + double? amount, }) = _GooglePayInitParams; factory GooglePayInitParams.fromJson(Map json) => diff --git a/packages/stripe_platform_interface/lib/src/models/google_pay.freezed.dart b/packages/stripe_platform_interface/lib/src/models/google_pay.freezed.dart index e45ffe042..64fa24efb 100644 --- a/packages/stripe_platform_interface/lib/src/models/google_pay.freezed.dart +++ b/packages/stripe_platform_interface/lib/src/models/google_pay.freezed.dart @@ -249,6 +249,12 @@ mixin _$GooglePayInitParams { /// wallet has existing payment methods. bool get existingPaymentMethodRequired => throw _privateConstructorUsedError; + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? get label => throw _privateConstructorUsedError; + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + double? get amount => throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $GooglePayInitParamsCopyWith get copyWith => @@ -267,7 +273,9 @@ abstract class $GooglePayInitParamsCopyWith<$Res> { bool testEnv, BillingAddressConfig? billingAddressConfig, bool isEmailRequired, - bool existingPaymentMethodRequired}); + bool existingPaymentMethodRequired, + String? label, + double? amount}); $BillingAddressConfigCopyWith<$Res>? get billingAddressConfig; } @@ -291,6 +299,8 @@ class _$GooglePayInitParamsCopyWithImpl<$Res, $Val extends GooglePayInitParams> Object? billingAddressConfig = freezed, Object? isEmailRequired = null, Object? existingPaymentMethodRequired = null, + Object? label = freezed, + Object? amount = freezed, }) { return _then(_value.copyWith( merchantName: null == merchantName @@ -317,6 +327,14 @@ class _$GooglePayInitParamsCopyWithImpl<$Res, $Val extends GooglePayInitParams> ? _value.existingPaymentMethodRequired : existingPaymentMethodRequired // ignore: cast_nullable_to_non_nullable as bool, + label: freezed == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String?, + amount: freezed == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as double?, ) as $Val); } @@ -348,7 +366,9 @@ abstract class _$$_GooglePayInitParamsCopyWith<$Res> bool testEnv, BillingAddressConfig? billingAddressConfig, bool isEmailRequired, - bool existingPaymentMethodRequired}); + bool existingPaymentMethodRequired, + String? label, + double? amount}); @override $BillingAddressConfigCopyWith<$Res>? get billingAddressConfig; @@ -371,6 +391,8 @@ class __$$_GooglePayInitParamsCopyWithImpl<$Res> Object? billingAddressConfig = freezed, Object? isEmailRequired = null, Object? existingPaymentMethodRequired = null, + Object? label = freezed, + Object? amount = freezed, }) { return _then(_$_GooglePayInitParams( merchantName: null == merchantName @@ -397,6 +419,14 @@ class __$$_GooglePayInitParamsCopyWithImpl<$Res> ? _value.existingPaymentMethodRequired : existingPaymentMethodRequired // ignore: cast_nullable_to_non_nullable as bool, + label: freezed == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String?, + amount: freezed == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as double?, )); } } @@ -411,7 +441,9 @@ class _$_GooglePayInitParams implements _GooglePayInitParams { this.testEnv = false, this.billingAddressConfig, this.isEmailRequired = false, - this.existingPaymentMethodRequired = true}); + this.existingPaymentMethodRequired = true, + this.label, + this.amount}); factory _$_GooglePayInitParams.fromJson(Map json) => _$$_GooglePayInitParamsFromJson(json); @@ -444,9 +476,17 @@ class _$_GooglePayInitParams implements _GooglePayInitParams { @JsonKey() final bool existingPaymentMethodRequired; + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + @override + final String? label; + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + @override + final double? amount; + @override String toString() { - return 'GooglePayInitParams(merchantName: $merchantName, countryCode: $countryCode, testEnv: $testEnv, billingAddressConfig: $billingAddressConfig, isEmailRequired: $isEmailRequired, existingPaymentMethodRequired: $existingPaymentMethodRequired)'; + return 'GooglePayInitParams(merchantName: $merchantName, countryCode: $countryCode, testEnv: $testEnv, billingAddressConfig: $billingAddressConfig, isEmailRequired: $isEmailRequired, existingPaymentMethodRequired: $existingPaymentMethodRequired, label: $label, amount: $amount)'; } @override @@ -466,7 +506,9 @@ class _$_GooglePayInitParams implements _GooglePayInitParams { (identical(other.existingPaymentMethodRequired, existingPaymentMethodRequired) || other.existingPaymentMethodRequired == - existingPaymentMethodRequired)); + existingPaymentMethodRequired) && + (identical(other.label, label) || other.label == label) && + (identical(other.amount, amount) || other.amount == amount)); } @JsonKey(ignore: true) @@ -478,7 +520,9 @@ class _$_GooglePayInitParams implements _GooglePayInitParams { testEnv, billingAddressConfig, isEmailRequired, - existingPaymentMethodRequired); + existingPaymentMethodRequired, + label, + amount); @JsonKey(ignore: true) @override @@ -502,7 +546,9 @@ abstract class _GooglePayInitParams implements GooglePayInitParams { final bool testEnv, final BillingAddressConfig? billingAddressConfig, final bool isEmailRequired, - final bool existingPaymentMethodRequired}) = _$_GooglePayInitParams; + final bool existingPaymentMethodRequired, + final String? label, + final double? amount}) = _$_GooglePayInitParams; factory _GooglePayInitParams.fromJson(Map json) = _$_GooglePayInitParams.fromJson; @@ -533,6 +579,14 @@ abstract class _GooglePayInitParams implements GooglePayInitParams { /// wallet has existing payment methods. bool get existingPaymentMethodRequired; @override + + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? get label; + @override + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + double? get amount; + @override @JsonKey(ignore: true) _$$_GooglePayInitParamsCopyWith<_$_GooglePayInitParams> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stripe_platform_interface/lib/src/models/google_pay.g.dart b/packages/stripe_platform_interface/lib/src/models/google_pay.g.dart index 61b93367c..f45735a00 100644 --- a/packages/stripe_platform_interface/lib/src/models/google_pay.g.dart +++ b/packages/stripe_platform_interface/lib/src/models/google_pay.g.dart @@ -35,6 +35,8 @@ _$_GooglePayInitParams _$$_GooglePayInitParamsFromJson( isEmailRequired: json['isEmailRequired'] as bool? ?? false, existingPaymentMethodRequired: json['existingPaymentMethodRequired'] as bool? ?? true, + label: json['label'] as String?, + amount: (json['amount'] as num?)?.toDouble(), ); Map _$$_GooglePayInitParamsToJson( @@ -46,6 +48,8 @@ Map _$$_GooglePayInitParamsToJson( 'billingAddressConfig': instance.billingAddressConfig?.toJson(), 'isEmailRequired': instance.isEmailRequired, 'existingPaymentMethodRequired': instance.existingPaymentMethodRequired, + 'label': instance.label, + 'amount': instance.amount, }; _$_BillingAddressConfig _$$_BillingAddressConfigFromJson( diff --git a/packages/stripe_platform_interface/lib/src/models/payment_sheet.dart b/packages/stripe_platform_interface/lib/src/models/payment_sheet.dart index 9e60b31ce..c274595ec 100644 --- a/packages/stripe_platform_interface/lib/src/models/payment_sheet.dart +++ b/packages/stripe_platform_interface/lib/src/models/payment_sheet.dart @@ -87,6 +87,9 @@ class SetupPaymentSheetParameters with _$SetupPaymentSheetParameters { /// Configuration for how billing details are collected during checkout. BillingDetailsCollectionConfiguration? billingDetailsCollectionConfiguration, + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? removeSavedPaymentMethodMessage, }) = _SetupParameters; factory SetupPaymentSheetParameters.fromJson(Map json) => @@ -178,6 +181,12 @@ class PaymentSheetGooglePay with _$PaymentSheetGooglePay { /// Whether or not to use the google pay test environment. Set to `true` until you have applied for and been granted access to the Production environment. @Default(false) bool testEnv, + + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? label, + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + String? amount, }) = _PaymentSheetGooglePay; factory PaymentSheetGooglePay.fromJson(Map json) => diff --git a/packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart b/packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart index 3d0d81290..8c6683022 100644 --- a/packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart +++ b/packages/stripe_platform_interface/lib/src/models/payment_sheet.freezed.dart @@ -97,6 +97,10 @@ mixin _$SetupPaymentSheetParameters { get billingDetailsCollectionConfiguration => throw _privateConstructorUsedError; + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? get removeSavedPaymentMethodMessage => + throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $SetupPaymentSheetParametersCopyWith @@ -128,7 +132,8 @@ abstract class $SetupPaymentSheetParametersCopyWith<$Res> { @JsonKey(name: 'defaultBillingDetails') BillingDetails? billingDetails, String? returnURL, BillingDetailsCollectionConfiguration? - billingDetailsCollectionConfiguration}); + billingDetailsCollectionConfiguration, + String? removeSavedPaymentMethodMessage}); $IntentConfigurationCopyWith<$Res>? get intentConfiguration; $PaymentSheetApplePayCopyWith<$Res>? get applePay; @@ -169,6 +174,7 @@ class _$SetupPaymentSheetParametersCopyWithImpl<$Res, Object? billingDetails = freezed, Object? returnURL = freezed, Object? billingDetailsCollectionConfiguration = freezed, + Object? removeSavedPaymentMethodMessage = freezed, }) { return _then(_value.copyWith( customFlow: null == customFlow @@ -236,6 +242,11 @@ class _$SetupPaymentSheetParametersCopyWithImpl<$Res, ? _value.billingDetailsCollectionConfiguration : billingDetailsCollectionConfiguration // ignore: cast_nullable_to_non_nullable as BillingDetailsCollectionConfiguration?, + removeSavedPaymentMethodMessage: freezed == + removeSavedPaymentMethodMessage + ? _value.removeSavedPaymentMethodMessage + : removeSavedPaymentMethodMessage // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } @@ -341,7 +352,8 @@ abstract class _$$_SetupParametersCopyWith<$Res> @JsonKey(name: 'defaultBillingDetails') BillingDetails? billingDetails, String? returnURL, BillingDetailsCollectionConfiguration? - billingDetailsCollectionConfiguration}); + billingDetailsCollectionConfiguration, + String? removeSavedPaymentMethodMessage}); @override $IntentConfigurationCopyWith<$Res>? get intentConfiguration; @@ -385,6 +397,7 @@ class __$$_SetupParametersCopyWithImpl<$Res> Object? billingDetails = freezed, Object? returnURL = freezed, Object? billingDetailsCollectionConfiguration = freezed, + Object? removeSavedPaymentMethodMessage = freezed, }) { return _then(_$_SetupParameters( customFlow: null == customFlow @@ -452,6 +465,11 @@ class __$$_SetupParametersCopyWithImpl<$Res> ? _value.billingDetailsCollectionConfiguration : billingDetailsCollectionConfiguration // ignore: cast_nullable_to_non_nullable as BillingDetailsCollectionConfiguration?, + removeSavedPaymentMethodMessage: freezed == + removeSavedPaymentMethodMessage + ? _value.removeSavedPaymentMethodMessage + : removeSavedPaymentMethodMessage // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -476,7 +494,8 @@ class _$_SetupParameters implements _SetupParameters { this.appearance, @JsonKey(name: 'defaultBillingDetails') this.billingDetails, this.returnURL, - this.billingDetailsCollectionConfiguration}); + this.billingDetailsCollectionConfiguration, + this.removeSavedPaymentMethodMessage}); factory _$_SetupParameters.fromJson(Map json) => _$$_SetupParametersFromJson(json); @@ -573,9 +592,13 @@ class _$_SetupParameters implements _SetupParameters { final BillingDetailsCollectionConfiguration? billingDetailsCollectionConfiguration; + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + @override + final String? removeSavedPaymentMethodMessage; + @override String toString() { - return 'SetupPaymentSheetParameters(customFlow: $customFlow, customerId: $customerId, primaryButtonLabel: $primaryButtonLabel, customerEphemeralKeySecret: $customerEphemeralKeySecret, paymentIntentClientSecret: $paymentIntentClientSecret, setupIntentClientSecret: $setupIntentClientSecret, intentConfiguration: $intentConfiguration, merchantDisplayName: $merchantDisplayName, applePay: $applePay, style: $style, googlePay: $googlePay, allowsDelayedPaymentMethods: $allowsDelayedPaymentMethods, appearance: $appearance, billingDetails: $billingDetails, returnURL: $returnURL, billingDetailsCollectionConfiguration: $billingDetailsCollectionConfiguration)'; + return 'SetupPaymentSheetParameters(customFlow: $customFlow, customerId: $customerId, primaryButtonLabel: $primaryButtonLabel, customerEphemeralKeySecret: $customerEphemeralKeySecret, paymentIntentClientSecret: $paymentIntentClientSecret, setupIntentClientSecret: $setupIntentClientSecret, intentConfiguration: $intentConfiguration, merchantDisplayName: $merchantDisplayName, applePay: $applePay, style: $style, googlePay: $googlePay, allowsDelayedPaymentMethods: $allowsDelayedPaymentMethods, appearance: $appearance, billingDetails: $billingDetails, returnURL: $returnURL, billingDetailsCollectionConfiguration: $billingDetailsCollectionConfiguration, removeSavedPaymentMethodMessage: $removeSavedPaymentMethodMessage)'; } @override @@ -605,8 +628,7 @@ class _$_SetupParameters implements _SetupParameters { (identical(other.style, style) || other.style == style) && (identical(other.googlePay, googlePay) || other.googlePay == googlePay) && - (identical(other.allowsDelayedPaymentMethods, - allowsDelayedPaymentMethods) || + (identical(other.allowsDelayedPaymentMethods, allowsDelayedPaymentMethods) || other.allowsDelayedPaymentMethods == allowsDelayedPaymentMethods) && (identical(other.appearance, appearance) || @@ -618,7 +640,10 @@ class _$_SetupParameters implements _SetupParameters { (identical(other.billingDetailsCollectionConfiguration, billingDetailsCollectionConfiguration) || other.billingDetailsCollectionConfiguration == - billingDetailsCollectionConfiguration)); + billingDetailsCollectionConfiguration) && + (identical(other.removeSavedPaymentMethodMessage, removeSavedPaymentMethodMessage) || + other.removeSavedPaymentMethodMessage == + removeSavedPaymentMethodMessage)); } @JsonKey(ignore: true) @@ -640,7 +665,8 @@ class _$_SetupParameters implements _SetupParameters { appearance, billingDetails, returnURL, - billingDetailsCollectionConfiguration); + billingDetailsCollectionConfiguration, + removeSavedPaymentMethodMessage); @JsonKey(ignore: true) @override @@ -675,7 +701,8 @@ abstract class _SetupParameters implements SetupPaymentSheetParameters { final BillingDetails? billingDetails, final String? returnURL, final BillingDetailsCollectionConfiguration? - billingDetailsCollectionConfiguration}) = _$_SetupParameters; + billingDetailsCollectionConfiguration, + final String? removeSavedPaymentMethodMessage}) = _$_SetupParameters; factory _SetupParameters.fromJson(Map json) = _$_SetupParameters.fromJson; @@ -771,6 +798,10 @@ abstract class _SetupParameters implements SetupPaymentSheetParameters { BillingDetailsCollectionConfiguration? get billingDetailsCollectionConfiguration; @override + + /// Optional configuration to display a custom message when a saved payment method is removed. iOS only. + String? get removeSavedPaymentMethodMessage; + @override @JsonKey(ignore: true) _$$_SetupParametersCopyWith<_$_SetupParameters> get copyWith => throw _privateConstructorUsedError; @@ -1561,6 +1592,12 @@ mixin _$PaymentSheetGooglePay { /// Whether or not to use the google pay test environment. Set to `true` until you have applied for and been granted access to the Production environment. bool get testEnv => throw _privateConstructorUsedError; + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? get label => throw _privateConstructorUsedError; + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + String? get amount => throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $PaymentSheetGooglePayCopyWith get copyWith => @@ -1573,7 +1610,12 @@ abstract class $PaymentSheetGooglePayCopyWith<$Res> { $Res Function(PaymentSheetGooglePay) then) = _$PaymentSheetGooglePayCopyWithImpl<$Res, PaymentSheetGooglePay>; @useResult - $Res call({String merchantCountryCode, String? currencyCode, bool testEnv}); + $Res call( + {String merchantCountryCode, + String? currencyCode, + bool testEnv, + String? label, + String? amount}); } /// @nodoc @@ -1593,6 +1635,8 @@ class _$PaymentSheetGooglePayCopyWithImpl<$Res, Object? merchantCountryCode = null, Object? currencyCode = freezed, Object? testEnv = null, + Object? label = freezed, + Object? amount = freezed, }) { return _then(_value.copyWith( merchantCountryCode: null == merchantCountryCode @@ -1607,6 +1651,14 @@ class _$PaymentSheetGooglePayCopyWithImpl<$Res, ? _value.testEnv : testEnv // ignore: cast_nullable_to_non_nullable as bool, + label: freezed == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String?, + amount: freezed == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } } @@ -1619,7 +1671,12 @@ abstract class _$$_PaymentSheetGooglePayCopyWith<$Res> __$$_PaymentSheetGooglePayCopyWithImpl<$Res>; @override @useResult - $Res call({String merchantCountryCode, String? currencyCode, bool testEnv}); + $Res call( + {String merchantCountryCode, + String? currencyCode, + bool testEnv, + String? label, + String? amount}); } /// @nodoc @@ -1636,6 +1693,8 @@ class __$$_PaymentSheetGooglePayCopyWithImpl<$Res> Object? merchantCountryCode = null, Object? currencyCode = freezed, Object? testEnv = null, + Object? label = freezed, + Object? amount = freezed, }) { return _then(_$_PaymentSheetGooglePay( merchantCountryCode: null == merchantCountryCode @@ -1650,6 +1709,14 @@ class __$$_PaymentSheetGooglePayCopyWithImpl<$Res> ? _value.testEnv : testEnv // ignore: cast_nullable_to_non_nullable as bool, + label: freezed == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String?, + amount: freezed == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -1661,7 +1728,9 @@ class _$_PaymentSheetGooglePay implements _PaymentSheetGooglePay { const _$_PaymentSheetGooglePay( {required this.merchantCountryCode, this.currencyCode, - this.testEnv = false}); + this.testEnv = false, + this.label, + this.amount}); factory _$_PaymentSheetGooglePay.fromJson(Map json) => _$$_PaymentSheetGooglePayFromJson(json); @@ -1679,9 +1748,17 @@ class _$_PaymentSheetGooglePay implements _PaymentSheetGooglePay { @JsonKey() final bool testEnv; + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + @override + final String? label; + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + @override + final String? amount; + @override String toString() { - return 'PaymentSheetGooglePay(merchantCountryCode: $merchantCountryCode, currencyCode: $currencyCode, testEnv: $testEnv)'; + return 'PaymentSheetGooglePay(merchantCountryCode: $merchantCountryCode, currencyCode: $currencyCode, testEnv: $testEnv, label: $label, amount: $amount)'; } @override @@ -1693,13 +1770,15 @@ class _$_PaymentSheetGooglePay implements _PaymentSheetGooglePay { other.merchantCountryCode == merchantCountryCode) && (identical(other.currencyCode, currencyCode) || other.currencyCode == currencyCode) && - (identical(other.testEnv, testEnv) || other.testEnv == testEnv)); + (identical(other.testEnv, testEnv) || other.testEnv == testEnv) && + (identical(other.label, label) || other.label == label) && + (identical(other.amount, amount) || other.amount == amount)); } @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, merchantCountryCode, currencyCode, testEnv); + int get hashCode => Object.hash( + runtimeType, merchantCountryCode, currencyCode, testEnv, label, amount); @JsonKey(ignore: true) @override @@ -1720,7 +1799,9 @@ abstract class _PaymentSheetGooglePay implements PaymentSheetGooglePay { const factory _PaymentSheetGooglePay( {required final String merchantCountryCode, final String? currencyCode, - final bool testEnv}) = _$_PaymentSheetGooglePay; + final bool testEnv, + final String? label, + final String? amount}) = _$_PaymentSheetGooglePay; factory _PaymentSheetGooglePay.fromJson(Map json) = _$_PaymentSheetGooglePay.fromJson; @@ -1738,6 +1819,14 @@ abstract class _PaymentSheetGooglePay implements PaymentSheetGooglePay { /// Whether or not to use the google pay test environment. Set to `true` until you have applied for and been granted access to the Production environment. bool get testEnv; @override + + /// An optional label to display with the amount. Google Pay may or may not display this label depending on its own internal logic. Defaults to a generic label if none is provided. + String? get label; + @override + + /// An optional amount to display for setup intents. Google Pay may or may not display this amount depending on its own internal logic. Defaults to 0 if none is provided. + String? get amount; + @override @JsonKey(ignore: true) _$$_PaymentSheetGooglePayCopyWith<_$_PaymentSheetGooglePay> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart b/packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart index cd01d0d04..88171e318 100644 --- a/packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart +++ b/packages/stripe_platform_interface/lib/src/models/payment_sheet.g.dart @@ -45,6 +45,8 @@ _$_SetupParameters _$$_SetupParametersFromJson(Map json) => : BillingDetailsCollectionConfiguration.fromJson( json['billingDetailsCollectionConfiguration'] as Map), + removeSavedPaymentMethodMessage: + json['removeSavedPaymentMethodMessage'] as String?, ); Map _$$_SetupParametersToJson(_$_SetupParameters instance) => @@ -66,6 +68,8 @@ Map _$$_SetupParametersToJson(_$_SetupParameters instance) => 'returnURL': instance.returnURL, 'billingDetailsCollectionConfiguration': instance.billingDetailsCollectionConfiguration?.toJson(), + 'removeSavedPaymentMethodMessage': + instance.removeSavedPaymentMethodMessage, }; const _$ThemeModeEnumMap = { @@ -172,6 +176,8 @@ _$_PaymentSheetGooglePay _$$_PaymentSheetGooglePayFromJson( merchantCountryCode: json['merchantCountryCode'] as String, currencyCode: json['currencyCode'] as String?, testEnv: json['testEnv'] as bool? ?? false, + label: json['label'] as String?, + amount: json['amount'] as String?, ); Map _$$_PaymentSheetGooglePayToJson( @@ -180,6 +186,8 @@ Map _$$_PaymentSheetGooglePayToJson( 'merchantCountryCode': instance.merchantCountryCode, 'currencyCode': instance.currencyCode, 'testEnv': instance.testEnv, + 'label': instance.label, + 'amount': instance.amount, }; _$_PaymentSheetAppearance _$$_PaymentSheetAppearanceFromJson( 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 979e6fe59..1e3ffbf91 100644 --- a/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart +++ b/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart @@ -69,6 +69,17 @@ abstract class StripePlatform extends PlatformInterface { /// Confirm the payment on a payment sheet. Future confirmPaymentSheetPayment(); + /// Configure the payment sheet using [CustomerSheetInitParams] as config. + Future initCustomerSheet( + CustomerSheetInitParams params); + + /// Display the customersheet sheet. + Future presentCustomerSheet({ + CustomerSheetPresentParams? options, + }); + + Future retrieveCustomerSheetPaymentOptionSelection(); + Future openApplePaySetup(); Future createApplePayToken(Map payment); diff --git a/packages/stripe_platform_interface/lib/stripe_platform_interface.dart b/packages/stripe_platform_interface/lib/stripe_platform_interface.dart index 1c5d719c9..f0c4588f5 100644 --- a/packages/stripe_platform_interface/lib/stripe_platform_interface.dart +++ b/packages/stripe_platform_interface/lib/stripe_platform_interface.dart @@ -10,6 +10,7 @@ export 'src/models/capture_method.dart'; export 'src/models/card_details.dart'; export 'src/models/card_field_input.dart'; export 'src/models/create_token_data.dart'; +export 'src/models/customer_sheet.dart'; export 'src/models/errors.dart'; export 'src/models/financial_connections.dart'; export 'src/models/google_pay.dart'; 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 939622f43..3e357438d 100644 --- a/packages/stripe_platform_interface/test/method_channel_stripe_test.dart +++ b/packages/stripe_platform_interface/test/method_channel_stripe_test.dart @@ -101,7 +101,7 @@ void main() { 'id': 'cvcResultToken', 'type': 'Card', 'livemode': true, - 'created': 1630670419 + 'created': '1630670419' } }, ).methodChannel, @@ -574,5 +574,53 @@ void main() { ); }); }); + + group('init customer sheet', () { + late Completer completer; + setUp(() async { + completer = Completer(); + sut = MethodChannelStripe( + platformIsIos: false, + platformIsAndroid: true, + methodChannel: MethodChannelMock( + channelName: methodChannelName, + method: 'initCustomerSheet', + result: {}, + ).methodChannel, + ); + await sut + .initCustomerSheet( + const CustomerSheetInitParams( + customerId: 'customerId', + customerEphemeralKeySecret: 'customerEphemeralKeySecret'), + ) + .then((_) => completer.complete()); + }); + + test('It completes operation', () { + expect(completer.isCompleted, true); + }); + }); + + group('When customersheet is succesfull', () { + late Completer completer; + setUp(() async { + completer = Completer(); + sut = MethodChannelStripe( + platformIsIos: false, + platformIsAndroid: true, + methodChannel: MethodChannelMock( + channelName: methodChannelName, + method: 'presentCustomerSheet', + result: {}, + ).methodChannel, + ); + await sut.presentCustomerSheet().then((_) => completer.complete()); + }); + + test('It completes operation', () { + expect(completer.isCompleted, true); + }); + }); }); } diff --git a/packages/stripe_web/lib/src/web_stripe.dart b/packages/stripe_web/lib/src/web_stripe.dart index 4526b566c..f1b4c4d87 100644 --- a/packages/stripe_web/lib/src/web_stripe.dart +++ b/packages/stripe_web/lib/src/web_stripe.dart @@ -522,6 +522,24 @@ class WebStripe extends StripePlatform { {String? returnURL}) { throw WebUnsupportedError.method('handleNextActionForSetupIntent'); } + + @override + Future initCustomerSheet( + CustomerSheetInitParams params) { + throw WebUnsupportedError.method('initCustomerSheet'); + } + + @override + Future presentCustomerSheet( + {CustomerSheetPresentParams? options}) { + throw WebUnsupportedError.method('presentCustomerSheet'); + } + + @override + Future retrieveCustomerSheetPaymentOptionSelection() { + throw WebUnsupportedError.method( + 'retrieveCustomerSheetPaymentOptionSelection'); + } } class WebUnsupportedError extends Error implements UnsupportedError {