diff --git a/README.md b/README.md index e06b21d1..ae013689 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Quick Glance -- This Flutter plugin is a straight port from the tipsi-stripe plugin for React Native - we tried to +- This Flutter plugin is a straight port from the tipsi-stripe plugin for React Native - we tried to keep the API as close as possible, so the documentation applies this plugin. - Collect chargeable tokens from users' **Card Input** and** Apple & Google Pay**. - For **SCA** compliant apps, setup payment intents for later confirmation. @@ -33,15 +33,15 @@ keep the API as close as possible, so the documentation applies this plugin. ![Apple Pay](https://user-images.githubusercontent.com/7946558/65780165-02838700-e0fe-11e9-9db9-5fe4e44ed819.gif) -## Dependencies +## Installation ### Android & iOS - Create a Stripe account and project - Retrieve a publishable key from the Stripe dashboard - + ![Stripe Dashboard](https://miro.medium.com/max/847/1*GPDsrgR6RXYuRCWiGxIF1g.png) -### Android +### Android - Requires AndroidX Include support in android/gradle.properties @@ -49,7 +49,15 @@ Include support in android/gradle.properties android.useAndroidX=true android.enableJetifier=true ``` -For proper setup also have a look at: https://github.com/jonasbark/flutter_stripe_payment/issues/88#issuecomment-553798157 +For proper setup also have a look at: https://github.com/jonasbark/flutter_stripe_payment/issues/88#issuecomment-553798157 + +### Web + +Edit your `web/index.html` file to include at the end of the `` tag (before `main.dart.js`) the [stripe library](https://stripe.com/docs/stripe-js#setup): + +```html + +``` ## Documentation diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart new file mode 100644 index 00000000..5c6e3a0c --- /dev/null +++ b/example/lib/generated_plugin_registrant.dart @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: lines_longer_than_80_chars + +import 'package:stripe_payment/src/stripe_payment_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + StripePaymentPluginWeb.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/example/web/index.html b/example/web/index.html index 9b7a438f..698d6550 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -28,6 +28,7 @@ }); } + diff --git a/lib/src/android_pay_payment_request.dart b/lib/src/android_pay_payment_request.dart index 3c5a4176..ee0844dd 100644 --- a/lib/src/android_pay_payment_request.dart +++ b/lib/src/android_pay_payment_request.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - class AndroidPayPaymentRequest { bool? billingAddressRequired; String? currencyCode; diff --git a/lib/src/checkout.dart b/lib/src/checkout.dart new file mode 100644 index 00000000..39ef2345 --- /dev/null +++ b/lib/src/checkout.dart @@ -0,0 +1,211 @@ + +/// Wether the checkout includes at least a subscription or just plain items +enum CheckoutMode { + payment, + subscription, +} + +String _checkoutModeToString(CheckoutMode type) { + switch (type) { + case CheckoutMode.payment: return 'payment'; + case CheckoutMode.subscription: return 'subscription'; + } +} + +/// Describes the type of transaction being performed by Checkout in order to +/// customize relevant text on the page, such as the Submit button. +/// [SubmitType] can only be specified when using using line items or SKUs, +/// and not subscriptions. +/// +/// The default is auto. +enum SubmitType { + auto, + book, + donate, + pay, +} + +String _submitTypeToString(SubmitType type) { + switch (type) { + case SubmitType.auto: return 'auto'; + case SubmitType.book: return 'book'; + case SubmitType.donate: return 'donate'; + case SubmitType.pay: return 'pay'; + } +} + +/// Maps options that are passed to `redirectToCheckout`. +/// @see https://stripe.com/docs/js/checkout/redirect_to_checkout +class Checkout { + /// Using Uri instead of plain [String]. Use `Uri.dataFromString()` + /// to quickly transform your urls, if needed. + Uri? successUrl; + + /// Using Uri instead of plain [String]. Use `Uri.dataFromString()` + /// to quickly transform your urls, if needed. + Uri? cancelUrl; + + /// Client-server integration uses a sessionId to control + /// what - and at which price - is being purchased. + /// @see https://stripe.com/docs/api/checkout/sessions/create + String? sessionId; + + /// Client-only integrations. + List? lineItems; + + /// The mode of the Checkout Session, one of payment or subscription. + /// Required if using lineItems with the client-only integration. + CheckoutMode? mode; + + /// A unique string to reference the Checkout session. + /// This can be a customer ID, a cart ID, or similar. + /// It is included in the checkout.session.completed webhook and can be used + /// to fulfill the purchase. + String? clientReferenceId; + + /// The email address used to create the customer object. + /// If you already know your customer's email address, use this attribute + /// to prefill it on Checkout. + String? customerEmail; + + /// Specify whether Checkout should collect the customer’s billing address. + /// If set to required, Checkout will attempt to collect the customer’s billing address. + /// If not set or set to auto Checkout will only attempt to collect the billing + /// address when necessary. + String? billingAddressCollection; + + /// When set, provides configuration for Checkout to collect a shipping address + /// from a customer. + ShippingAddressCollection? shippingAddressCollection; + + /// A locale that will be used to localize the display of Checkout. + /// Default is auto (Stripe detects the locale of the browser). + String? locale; + + /// Describes the type of transaction being performed by Checkout in order to + /// customize relevant text on the page, such as the Submit button. + /// [SubmitType] can only be specified when using using line items or SKUs, + /// and not subscriptions. + SubmitType? submitType; + + Checkout({ + this.successUrl, + this.cancelUrl, + this.sessionId, + this.lineItems, + this.mode, + this.clientReferenceId, + this.billingAddressCollection = 'auto', + this.shippingAddressCollection, + this.locale = 'auto', + this.submitType = SubmitType.auto, + }) : + assert( + sessionId != null || lineItems != null, + 'Please provide either a sessionId or lineItems to proceed to checkout.' + ), + assert( + sessionId != null + || sessionId == null && mode != null, + 'Mode is mandatory for client-only integrations,' + ), + assert( + sessionId != null + || sessionId == null && successUrl != null, + 'successUrl is mandatory for client-only integrations,' + ), + assert( + sessionId != null + || sessionId == null && cancelUrl != null, + 'cancelUrl is mandatory for client-only integrations,' + ); + + Map toJson() { + final data = Map(); + + // Client + server integration + if (sessionId != null) { + data['sessionId'] = sessionId; + } else { + data['lineItems'] = lineItems! + .map((lineItem) => lineItem.toJson()); + data['mode'] = _checkoutModeToString(mode!); + data['successUrl'] = successUrl!.toString(); + data['cancelUrl'] = cancelUrl!.toString(); + + /// Optional fields (client-only integrations) + if (clientReferenceId != null) + data['clientReferenceId'] = clientReferenceId; + if (customerEmail != null) + data['customerEmail'] = customerEmail; + if (billingAddressCollection != null) + data['billingAddressCollection'] = billingAddressCollection; + if (shippingAddressCollection != null) + data['shippingAddressCollection'] = shippingAddressCollection!.toJson(); + if (locale != null) + data['locale'] = locale; + if (submitType != null) + data['submitType'] = _submitTypeToString(submitType!); + } + + return data; + } +} + +/// Maps the [Checkout] item +class CheckoutLineItem { + final String price; + final int quantity; + + CheckoutLineItem({ + required this.price, + required this.quantity, + }); + + Map toJson() => + { + 'price': price, + 'quantity': quantity, + }; +} + +class ShippingAddressCollection { + final List allowedCountries; + + ShippingAddressCollection(this.allowedCountries); + + Map toJson() => + { + 'allowedCountries': allowedCountries, + }; +} + +/// Boilerplate code to parse errors +class CheckoutResult { + CheckoutResultError? error; + + CheckoutResult({ this.error }); + + bool get hasError => + error != null; + + factory CheckoutResult.fromJson(Map? json) => + CheckoutResult( + error: json?.containsKey('error') == true + ? CheckoutResultError.fromJson(json!['error']) + : null + ); +} + +class CheckoutResultError { + final String message; + + CheckoutResultError(this.message); + + factory CheckoutResultError.fromJson(Map? json) => + CheckoutResultError( + json?.containsKey('message') == true + ? json!['message'] + : null + ); +} diff --git a/lib/src/source_params.dart b/lib/src/source_params.dart index 1dcc2130..fcd5c835 100644 --- a/lib/src/source_params.dart +++ b/lib/src/source_params.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:stripe_payment/src/token.dart'; class SourceParams { diff --git a/lib/src/stripe_payment.dart b/lib/src/stripe_payment.dart index de61e636..54fa9a39 100644 --- a/lib/src/stripe_payment.dart +++ b/lib/src/stripe_payment.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; +import 'package:stripe_payment/src/checkout.dart'; import 'android_pay_payment_request.dart'; import 'apple_pay_payment_request.dart'; @@ -174,6 +174,20 @@ class StripePayment { final result = await _channel.invokeMethod('confirmSetupIntent', intent.toJson()); return SetupIntentResult.fromJson(result); } + + static Future redirectToCheckout(Checkout checkout) async { + if (kIsWeb) { + final result = await _channel.invokeMethod( + 'redirectToCheckout', + checkout.toJson() + ); + return CheckoutResult.fromJson(result); + } + + throw UnimplementedError( + 'redirectToCheckout is not supported for environments other than web' + ); + } } class StripeOptions { diff --git a/lib/src/stripe_payment_web.dart b/lib/src/stripe_payment_web.dart new file mode 100644 index 00000000..63042a50 --- /dev/null +++ b/lib/src/stripe_payment_web.dart @@ -0,0 +1,70 @@ +import 'dart:js'; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +class StripePaymentPluginWeb { + JsObject? _stripeInstance; + + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'stripe_payment', + const StandardMethodCodec(), + registrar, + ); + final StripePaymentPluginWeb instance = StripePaymentPluginWeb(); + channel.setMethodCallHandler(instance.handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + /// Simple setup check + if (!context.hasProperty('Stripe')) { + throw PlatformException( + code: 'Missing JS dependency', + details: + 'The stripe JS library was not included in your "web/index.html" file. ' + 'Please follow the setup instructions in the README file of the flutter_stripe_payment project.', + ); + } + + /// Class initialization, allows dynamic Stripe loading (with + /// different env keys) + if (call.method == 'setOptions') { + _stripeInstance = JsObject( + context['Stripe'], + [ + // publishableKey is the first unnamed argument + call.arguments['options']['publishableKey'], + // options follow + JsObject.jsify(call.arguments['options']) + ] + ); + return; + } + + /// Any other method call with an uninitialized plugin will throw an exception + if (_stripeInstance == null) { + throw PlatformException( + code: 'Stripe not initialized', + details: + 'Trying to call a method before proper library initialization.' + 'Please ensure that you call "StripePayment.setOptions" before calling any other method from this library.', + ); + } + + switch (call.method) { + case 'redirectToCheckout': + _stripeInstance!.callMethod( + 'redirectToCheckout', + [JsObject.jsify(call.arguments),] + ); + break; + default: + throw PlatformException( + code: 'Unimplemented', + details: + "The stripe plugin for web doesn't implement the method '${call.method}'", + ); + } + } +} diff --git a/lib/stripe_payment.dart b/lib/stripe_payment.dart index 9eca62c3..37e96d38 100644 --- a/lib/stripe_payment.dart +++ b/lib/stripe_payment.dart @@ -8,3 +8,11 @@ export 'src/source.dart'; export 'src/source_params.dart'; export 'src/stripe_payment.dart'; export 'src/token.dart'; +export 'src/checkout.dart' show + Checkout, + CheckoutLineItem, + CheckoutMode, + SubmitType, + ShippingAddressCollection, + CheckoutResult, + CheckoutResultError; diff --git a/pubspec.yaml b/pubspec.yaml index 9751c4a9..d7d8a5f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,7 @@ name: stripe_payment -description: A Flutter plugin to integrate the stripe libraries for iOS and Android. Supports Apple / Google Pay, SCA, PSD2 and much more. -version: 1.1.0 +description: A Flutter plugin to integrate the stripe libraries for iOS, Android and Web. Supports Apple / Google Pay, SCA, PSD2 and much more. +version: 1.2.0 +publish_to: none homepage: https://github.com/jonasbark/flutter_stripe_payment environment: @@ -10,6 +11,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter flutter: plugin: @@ -19,3 +22,6 @@ flutter: pluginClass: StripePaymentPlugin ios: pluginClass: StripePaymentPlugin + web: + pluginClass: StripePaymentPluginWeb + fileName: src/stripe_payment_web.dart