Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,23 +33,31 @@ 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
```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 `<body>` tag (before `main.dart.js`) the [stripe library](https://stripe.com/docs/stripe-js#setup):

```html
<script src="https://js.stripe.com/v3/"></script>
```

## Documentation

Expand Down
15 changes: 15 additions & 0 deletions example/lib/generated_plugin_registrant.dart
Original file line number Diff line number Diff line change
@@ -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();
}
1 change: 1 addition & 0 deletions example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
});
}
</script>
<script src="https://js.stripe.com/v3/"></script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
2 changes: 0 additions & 2 deletions lib/src/android_pay_payment_request.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';

class AndroidPayPaymentRequest {
bool? billingAddressRequired;
String? currencyCode;
Expand Down
211 changes: 211 additions & 0 deletions lib/src/checkout.dart
Original file line number Diff line number Diff line change
@@ -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<CheckoutLineItem>? 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<String, Object?> toJson() {
final data = Map<String, dynamic>();

// 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<String, Object?> toJson() =>
{
'price': price,
'quantity': quantity,
};
}

class ShippingAddressCollection {
final List<String> allowedCountries;

ShippingAddressCollection(this.allowedCountries);

Map<String, Object?> toJson() =>
{
'allowedCountries': allowedCountries,
};
}

/// Boilerplate code to parse errors
class CheckoutResult {
CheckoutResultError? error;

CheckoutResult({ this.error });

bool get hasError =>
error != null;

factory CheckoutResult.fromJson(Map<String, dynamic>? json) =>
CheckoutResult(
error: json?.containsKey('error') == true
? CheckoutResultError.fromJson(json!['error'])
: null
);
}

class CheckoutResultError {
final String message;

CheckoutResultError(this.message);

factory CheckoutResultError.fromJson(Map<String, dynamic>? json) =>
CheckoutResultError(
json?.containsKey('message') == true
? json!['message']
: null
);
}
1 change: 0 additions & 1 deletion lib/src/source_params.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:stripe_payment/src/token.dart';

class SourceParams {
Expand Down
16 changes: 15 additions & 1 deletion lib/src/stripe_payment.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -174,6 +174,20 @@ class StripePayment {
final result = await _channel.invokeMethod('confirmSetupIntent', intent.toJson());
return SetupIntentResult.fromJson(result);
}

static Future<CheckoutResult> 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 {
Expand Down
70 changes: 70 additions & 0 deletions lib/src/stripe_payment_web.dart
Original file line number Diff line number Diff line change
@@ -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<dynamic> 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}'",
);
}
}
}
Loading