final2
This commit is contained in:
140
lib/utils/mini_stripe_api.dart
Normal file
140
lib/utils/mini_stripe_api.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
|
||||
|
||||
import '../vendor/stripe_sdk/lib/stripe_sdk.dart';
|
||||
import '../vendor/stripe_sdk/lib/stripe_sdk_ui.dart';
|
||||
|
||||
import 'mini_stripe_api_handler.dart';
|
||||
|
||||
class MiniStripeApi {
|
||||
static MiniStripeApi? _instance;
|
||||
|
||||
final MiniStripeApiHandler _apiHandler;
|
||||
|
||||
final String publishableKey;
|
||||
|
||||
final String apiVersion;
|
||||
|
||||
/// Create a new instance, which can be used with e.g. dependency injection.
|
||||
/// Throws a [Exception] if an invalid [publishableKey] has been submitted.
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "sk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
MiniStripeApi(this.publishableKey, {this.apiVersion = defaultApiVersion, String? stripeAccount})
|
||||
: _apiHandler = MiniStripeApiHandler(stripeAccount: stripeAccount) {
|
||||
_validateKey(publishableKey);
|
||||
_apiHandler.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
/// Initialize the managed singleton instance of [StripeApi].
|
||||
/// Afterwards you can use [StripeApi.instance] to access the created instance.
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "sk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
static void init(String publishableKey, {String apiVersion = defaultApiVersion, String? stripeAccount}) {
|
||||
// _instance ??= MiniStripeApi(publishableKey, uuidString, apiVersion: apiVersion, stripeAccount: stripeAccount);
|
||||
_instance = MiniStripeApi(publishableKey, apiVersion: apiVersion, stripeAccount: stripeAccount);
|
||||
}
|
||||
|
||||
/// Access the singleton instance of [StripeApi].
|
||||
/// Throws an [Exception] if [StripeApi.init] hasn't been called previously.
|
||||
static MiniStripeApi get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('Attempted to get singleton instance of StripeApi without initialization');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Create a stripe Token
|
||||
/// https://stripe.com/docs/api/tokens
|
||||
Future<Map<String, dynamic>> createToken(Map<String, dynamic> data) async {
|
||||
final path = '/tokens';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Create a PaymentMethod.
|
||||
/// https://stripe.com/docs/api/payment_methods/create
|
||||
Future<Map<String, dynamic>> createPaymentMethod(Map<String, dynamic> data) async {
|
||||
final path = '/payment_methods';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Create a PaymentMethod from a card.
|
||||
/// This will only create a PaymentMethod with the minimum required properties.
|
||||
/// To include additional properties such as billing details, use [StripeCard.toPaymentMethod], add additional details
|
||||
/// and then use [createPaymentMethod].
|
||||
Future<Map<String, dynamic>> createPaymentMethodFromCard(StripeCard card) async {
|
||||
return createPaymentMethod(card.toPaymentMethod());
|
||||
}
|
||||
|
||||
/// Create a new Source object.
|
||||
/// https://stripe.com/docs/api/sources/create
|
||||
Future<Map<String, dynamic>> createSource(Map<String, dynamic> data) async {
|
||||
final path = '/sources';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Retrieve a PaymentIntent.
|
||||
/// https://stripe.com/docs/api/payment_intents/retrieve
|
||||
Future<Map<String, dynamic>> retrievePaymentIntent(String clientSecret, {String? apiVersion}) async {
|
||||
final intentId = _parseIdFromClientSecret(clientSecret);
|
||||
final path = '/payment_intents/$intentId';
|
||||
final params = {'client_secret': clientSecret};
|
||||
return _apiHandler.request(RequestMethod.get, path, publishableKey, apiVersion ?? this.apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Confirm a PaymentIntent
|
||||
/// https://stripe.com/docs/api/payment_intents/confirm
|
||||
Future<Map<String, dynamic>> confirmPaymentIntent(String clientSecret, {Map<String, dynamic>? data, String? idempotencyKey}) async {
|
||||
final params = data ?? {};
|
||||
final intent = _parseIdFromClientSecret(clientSecret);
|
||||
params['client_secret'] = clientSecret;
|
||||
final path = '/payment_intents/$intent/confirm';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: params, idempotencyKey: idempotencyKey);
|
||||
}
|
||||
|
||||
/// Retrieve a SetupIntent.
|
||||
/// https://stripe.com/docs/api/setup_intents/retrieve
|
||||
Future<Map<String, dynamic>> retrieveSetupIntent(String clientSecret, {String? apiVersion}) async {
|
||||
final intentId = _parseIdFromClientSecret(clientSecret);
|
||||
final path = '/setup_intents/$intentId';
|
||||
final params = {'client_secret': clientSecret};
|
||||
return _apiHandler.request(RequestMethod.get, path, publishableKey, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Confirm a SetupIntent
|
||||
/// https://stripe.com/docs/api/setup_intents/confirm
|
||||
Future<Map<String, dynamic>> confirmSetupIntent(String clientSecret, {Map<String, dynamic>? data}) async {
|
||||
final params = data ?? {};
|
||||
final intent = _parseIdFromClientSecret(clientSecret);
|
||||
params['client_secret'] = clientSecret;
|
||||
final path = '/setup_intents/$intent/confirm';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Validates the received [publishableKey] and throws a [Exception] if an
|
||||
/// invalid key has been submitted.
|
||||
static void _validateKey(String publishableKey) {
|
||||
if (publishableKey == null || publishableKey.isEmpty) {
|
||||
throw Exception('Invalid Publishable Key: '
|
||||
'You must use a valid publishable key to create a token. '
|
||||
'For more info, see https://stripe.com/docs/stripe.js.');
|
||||
}
|
||||
|
||||
if (publishableKey.startsWith('sk_')) {
|
||||
throw Exception('Invalid Publishable Key: '
|
||||
'You are using a secret key to create a token, '
|
||||
'instead of the publishable one. For more info, '
|
||||
'see https://stripe.com/docs/stripe.js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _parseIdFromClientSecret(String clientSecret) {
|
||||
return clientSecret.split('_secret')[0];
|
||||
}
|
||||
164
lib/utils/mini_stripe_api_handler.dart
Normal file
164
lib/utils/mini_stripe_api_handler.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show json;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../vendor/stripe_sdk/lib/stripe_sdk.dart';
|
||||
|
||||
class MiniStripeApiHandler {
|
||||
static const String LIVE_API_BASE = 'https://api.stripe.com';
|
||||
static const String LIVE_LOGGING_BASE = 'https://q.stripe.com';
|
||||
static const String LOGGING_ENDPOINT = 'https://m.stripe.com/4';
|
||||
static const String LIVE_API_PATH = LIVE_API_BASE + '/v1';
|
||||
|
||||
static const String CHARSET = 'UTF-8';
|
||||
static const String CUSTOMERS = 'customers';
|
||||
static const String TOKENS = 'tokens';
|
||||
static const String SOURCES = 'sources';
|
||||
|
||||
static const String HEADER_KEY_REQUEST_ID = 'Request-Id';
|
||||
static const String FIELD_ERROR = 'error';
|
||||
static const String FIELD_SOURCE = 'source';
|
||||
|
||||
String apiVersion = defaultApiVersion;
|
||||
|
||||
static const String MALFORMED_RESPONSE_MESSAGE = 'An improperly formatted error response was found.';
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
final String? stripeAccount;
|
||||
|
||||
MiniStripeApiHandler({this.stripeAccount});
|
||||
|
||||
Future<Map<String, dynamic>> request(RequestMethod method, String path, String key, String? apiVersion,
|
||||
{final Map<String, dynamic>? params, String? idempotencyKey}) {
|
||||
final options = RequestOptions(key: key, apiVersion: apiVersion, stripeAccount: stripeAccount, idempotencyKey: idempotencyKey);
|
||||
return _getStripeResponse(method, LIVE_API_PATH + path, options, params: params);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getStripeResponse(RequestMethod method, final String url, final RequestOptions options,
|
||||
{final Map<String, dynamic>? params}) async {
|
||||
print('url: $url');
|
||||
final headers = _headers(options: options);
|
||||
|
||||
print('headers: $headers');
|
||||
http.Response response;
|
||||
|
||||
switch (method) {
|
||||
case RequestMethod.get:
|
||||
var fUrl = url;
|
||||
if (params != null && params.isNotEmpty) {
|
||||
fUrl = '$url?${_encodeMap(params)}';
|
||||
}
|
||||
response = await _client.get(Uri.parse(fUrl), headers: headers);
|
||||
break;
|
||||
|
||||
case RequestMethod.post:
|
||||
response = await _client.post(
|
||||
Uri.parse(url),
|
||||
headers: headers,
|
||||
body: params != null ? _urlEncodeMap(params) : null,
|
||||
);
|
||||
break;
|
||||
|
||||
case RequestMethod.delete:
|
||||
response = await _client.delete(Uri.parse(url), headers: headers);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Request Method: $method not implemented');
|
||||
}
|
||||
|
||||
final requestId = response.headers[HEADER_KEY_REQUEST_ID];
|
||||
|
||||
final statusCode = response.statusCode;
|
||||
Map<String, dynamic> resp;
|
||||
try {
|
||||
resp = json.decode(response.body);
|
||||
} catch (error) {
|
||||
final stripeError = StripeApiError(requestId, {StripeApiError.fieldMessage: MALFORMED_RESPONSE_MESSAGE});
|
||||
throw StripeApiException(stripeError);
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
final Map<String, dynamic> errBody = resp[FIELD_ERROR];
|
||||
final stripeError = StripeApiError(requestId, errBody);
|
||||
throw StripeApiException(stripeError);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
static Map<String, String> _headers({RequestOptions? options}) {
|
||||
final headers = <String, String>{};
|
||||
headers['Accept-Charset'] = CHARSET;
|
||||
headers['Accept'] = 'application/json';
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
headers['User-Agent'] = 'StripeSDK/v2';
|
||||
|
||||
if (options != null) {
|
||||
headers['Authorization'] = 'Bearer ${options.key}';
|
||||
}
|
||||
|
||||
// debug headers
|
||||
final propertyMap = <String, String>{};
|
||||
propertyMap['os.name'] = defaultTargetPlatform.toString();
|
||||
propertyMap['lang'] = 'Dart';
|
||||
propertyMap['publisher'] = 'lars.dahl@gmail.com';
|
||||
|
||||
headers['X-Stripe-Client-User-Agent'] = json.encode(propertyMap);
|
||||
|
||||
if (options != null) {
|
||||
if (options.apiVersion != null) {
|
||||
headers['Stripe-Version'] = options.apiVersion!;
|
||||
}
|
||||
|
||||
if (options.stripeAccount != null) {
|
||||
headers['Stripe-Account'] = options.stripeAccount!;
|
||||
}
|
||||
|
||||
if (options.idempotencyKey != null) {
|
||||
headers['Idempotency-Key'] = options.idempotencyKey!;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
static String _encodeMap(Map<String, dynamic> params) {
|
||||
return params.keys
|
||||
.map((key) => '${Uri.encodeComponent(key)}=${Uri.encodeComponent(params[key].toString())}')
|
||||
.join('&');
|
||||
}
|
||||
|
||||
static String _urlEncodeMap(dynamic data) {
|
||||
final urlData = StringBuffer('');
|
||||
var first = true;
|
||||
void urlEncode(dynamic sub, String path) {
|
||||
if (sub is List) {
|
||||
for (var i = 0; i < sub.length; i++) {
|
||||
urlEncode(sub[i], '$path%5B%5D');
|
||||
}
|
||||
} else if (sub is Map) {
|
||||
sub.forEach((k, v) {
|
||||
if (path == '') {
|
||||
urlEncode(v, '${Uri.encodeQueryComponent(k)}');
|
||||
} else {
|
||||
urlEncode(v, '$path%5B${Uri.encodeQueryComponent(k)}%5D');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!first) {
|
||||
urlData.write('&');
|
||||
}
|
||||
first = false;
|
||||
urlData.write('$path=${Uri.encodeQueryComponent(sub.toString())}');
|
||||
}
|
||||
}
|
||||
|
||||
urlEncode(data, '');
|
||||
return urlData.toString();
|
||||
}
|
||||
}
|
||||
7
lib/vendor/countdown/countdown.dart
vendored
Normal file
7
lib/vendor/countdown/countdown.dart
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015, Benjamin NGUYEN. All rights reserved. Use of this source code
|
||||
// is governed by a BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// The countdown library.
|
||||
library countdown;
|
||||
|
||||
export 'src/countdown_base.dart';
|
||||
90
lib/vendor/countdown/src/countdown_base.dart
vendored
Normal file
90
lib/vendor/countdown/src/countdown_base.dart
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015, Benjamin NGUYEN. All rights reserved. Use of this source code
|
||||
// is governed by a BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
library countdown.base;
|
||||
|
||||
import "dart:async";
|
||||
|
||||
class CountDown {
|
||||
/// reference point for start and resume
|
||||
DateTime? _begin;
|
||||
Timer? _timer;
|
||||
Duration? _duration;
|
||||
Duration? remainingTime;
|
||||
bool isPaused = false;
|
||||
StreamController<Duration>? _controller;
|
||||
Duration? _refresh;
|
||||
|
||||
/// provide a way to send less data to the client but keep the data of the timer up to date
|
||||
int _everyTick = 0;
|
||||
int counter = 0;
|
||||
|
||||
/// once you instantiate the CountDown you need to register to receive information
|
||||
CountDown(Duration duration,
|
||||
{Duration refresh = const Duration(milliseconds: 10), int everyTick = 1}) {
|
||||
_refresh = refresh;
|
||||
_everyTick = everyTick;
|
||||
|
||||
this._duration = duration;
|
||||
_controller = StreamController<Duration>(
|
||||
onListen: _onListen,
|
||||
onPause: _onPause,
|
||||
onResume: _onResume,
|
||||
onCancel: _onCancel);
|
||||
}
|
||||
|
||||
Stream<Duration> get stream => _controller!.stream;
|
||||
|
||||
/// _onListen
|
||||
/// invoke when the first subscriber has subscribe and not before to avoid leak of memory
|
||||
_onListen() {
|
||||
// reference point
|
||||
_begin = DateTime.now();
|
||||
_timer = Timer.periodic(_refresh!, _tick);
|
||||
}
|
||||
|
||||
/// the remaining time is set at '_refresh' ms accurate
|
||||
_onPause() {
|
||||
isPaused = true;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
/// ...restart the timer with the duration
|
||||
_onResume() {
|
||||
_begin = DateTime.now();
|
||||
|
||||
_duration = this.remainingTime;
|
||||
isPaused = false;
|
||||
|
||||
// lance le timer
|
||||
_timer = Timer.periodic(_refresh!, _tick);
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
// on pause we already cancel the _timer
|
||||
if (!isPaused) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
// _controller.close(); // close automatically the "pipe" when the sub close it by sub.cancel()
|
||||
}
|
||||
|
||||
void _tick(Timer? timer) {
|
||||
counter++;
|
||||
Duration alreadyConsumed = DateTime.now().difference(_begin!);
|
||||
this.remainingTime = this._duration! - alreadyConsumed;
|
||||
if (this.remainingTime!.isNegative) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
// tell the onDone's subscriber that it's finish
|
||||
_controller?.close();
|
||||
} else {
|
||||
// here we can control the frequency of sending data
|
||||
if (counter % _everyTick == 0) {
|
||||
_controller?.add(this.remainingTime!);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
lib/vendor/double_back_to_close_app/double_back_to_close_app.dart
vendored
Normal file
98
lib/vendor/double_back_to_close_app/double_back_to_close_app.dart
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Allows the user to close the app by double tapping the back-button.
|
||||
///
|
||||
/// You must specify a [SnackBar], so it can be shown when the user taps the
|
||||
/// back-button. Notice that the value you set for [SnackBar.duration] is going
|
||||
/// to be considered to decide whether the snack-bar is currently visible or
|
||||
/// not.
|
||||
///
|
||||
/// Since the back-button is an Android feature, this Widget is going to be
|
||||
/// nothing but the own [child] if the current platform is anything but Android.
|
||||
class DoubleBackToCloseApp extends StatefulWidget {
|
||||
/// The [SnackBar] shown when the user taps the back-button.
|
||||
final SnackBar snackBar;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget child;
|
||||
|
||||
/// Creates a widget that allows the user to close the app by double tapping
|
||||
/// the back-button.
|
||||
const DoubleBackToCloseApp({
|
||||
Key? key,
|
||||
required this.snackBar,
|
||||
required this.child,
|
||||
}) : assert(snackBar != null),
|
||||
assert(child != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_DoubleBackToCloseAppState createState() => _DoubleBackToCloseAppState();
|
||||
}
|
||||
|
||||
class _DoubleBackToCloseAppState extends State<DoubleBackToCloseApp> {
|
||||
/// The last time the user tapped Android's back-button.
|
||||
DateTime? _lastTimeBackButtonWasTapped;
|
||||
|
||||
/// Returns whether the current platform is Android.
|
||||
bool get _isAndroid => Theme.of(context).platform == TargetPlatform.android;
|
||||
|
||||
/// Returns whether the [DoubleBackToCloseApp.snackBar] is currently visible.
|
||||
///
|
||||
/// The snack-bar is going to be considered visible if the duration of the
|
||||
/// snack-bar is greater than the difference from now to the
|
||||
/// [_lastTimeBackButtonWasTapped].
|
||||
///
|
||||
/// This is not quite accurate since the snack-bar could've been dismissed by
|
||||
/// the user, so this algorithm needs to be improved, as described in #2.
|
||||
bool get _isSnackBarVisible =>
|
||||
(_lastTimeBackButtonWasTapped != null) &&
|
||||
(widget.snackBar.duration >
|
||||
DateTime.now().difference(_lastTimeBackButtonWasTapped!));
|
||||
|
||||
/// Returns whether the next back navigation of this route will be handled
|
||||
/// internally.
|
||||
///
|
||||
/// Returns true when there's a widget that inserted an entry into the
|
||||
/// local-history of the current route, in order to handle pop. This is done
|
||||
/// by [Drawer], for example, so it can close on pop.
|
||||
bool get _willHandlePopInternally =>
|
||||
ModalRoute.of(context)!.willHandlePopInternally;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_ensureThatContextContainsScaffold();
|
||||
|
||||
if (_isAndroid) {
|
||||
return WillPopScope(
|
||||
onWillPop: _handleWillPop,
|
||||
child: widget.child,
|
||||
);
|
||||
} else {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles [WillPopScope.onWillPop].
|
||||
Future<bool> _handleWillPop() async {
|
||||
if (_isSnackBarVisible || _willHandlePopInternally) {
|
||||
return true;
|
||||
} else {
|
||||
_lastTimeBackButtonWasTapped = DateTime.now();
|
||||
// Scaffold.of(context).showSnackBar(widget.snackBar);
|
||||
ScaffoldMessenger.of(context).showSnackBar(widget.snackBar);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws a [FlutterError] if this widget was not wrapped in a [Scaffold].
|
||||
void _ensureThatContextContainsScaffold() {
|
||||
if (Scaffold.maybeOf(context) == null) {
|
||||
throw FlutterError(
|
||||
'`DoubleBackToCloseApp` must be wrapped in a `Scaffold`.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
449
lib/vendor/flappy_search_bar/flappy_search_bar.dart
vendored
Normal file
449
lib/vendor/flappy_search_bar/flappy_search_bar.dart
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
library flappy_search_bar;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'scaled_tile.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
import 'search_bar_style.dart';
|
||||
|
||||
mixin _ControllerListener<T> on State<SearchBar<T>> {
|
||||
void onListChanged(List<T> items) {}
|
||||
|
||||
void onLoading() {}
|
||||
|
||||
void onClear() {}
|
||||
|
||||
void onError(Error error) {}
|
||||
}
|
||||
|
||||
class SearchBarController<T> {
|
||||
final List<T> _list = [];
|
||||
final List<T> _filteredList = [];
|
||||
final List<T> _sortedList = [];
|
||||
TextEditingController? _searchQueryController;
|
||||
String? _lastSearchedText;
|
||||
Future<List<T>> Function(String text)? _lastSearchFunction;
|
||||
_ControllerListener? _controllerListener;
|
||||
int Function(T a, T b)? _lastSorting;
|
||||
CancelableOperation? _cancelableOperation;
|
||||
int? minimumChars;
|
||||
|
||||
void setTextController(TextEditingController _searchQueryController, minimunChars) {
|
||||
this._searchQueryController = _searchQueryController;
|
||||
this.minimumChars = minimunChars;
|
||||
}
|
||||
|
||||
void setListener(_ControllerListener _controllerListener) {
|
||||
this._controllerListener = _controllerListener;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_controllerListener?.onClear();
|
||||
}
|
||||
|
||||
void _search(
|
||||
String text, Future<List<T>> Function(String text) onSearch) async {
|
||||
_controllerListener?.onLoading();
|
||||
try {
|
||||
if (_cancelableOperation != null &&
|
||||
(!_cancelableOperation!.isCompleted ||
|
||||
!_cancelableOperation!.isCanceled)) {
|
||||
_cancelableOperation!.cancel();
|
||||
}
|
||||
_cancelableOperation = CancelableOperation.fromFuture(
|
||||
onSearch(text),
|
||||
onCancel: () => {},
|
||||
);
|
||||
|
||||
final List<T> items = await _cancelableOperation!.value;
|
||||
_lastSearchFunction = onSearch;
|
||||
_lastSearchedText = text;
|
||||
_list.clear();
|
||||
_filteredList.clear();
|
||||
_sortedList.clear();
|
||||
_lastSorting = null;
|
||||
_list.addAll(items);
|
||||
_controllerListener?.onListChanged(_list);
|
||||
} catch (error) {
|
||||
_controllerListener?.onError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
void injectSearch(
|
||||
String searchText, Future<List<T>> Function(String text) onSearch) {
|
||||
if (searchText != null && searchText.length >= minimumChars!) {
|
||||
_searchQueryController?.text = searchText;
|
||||
_search(searchText, onSearch);
|
||||
}
|
||||
}
|
||||
|
||||
void replayLastSearch() {
|
||||
if (_lastSearchFunction != null && _lastSearchedText != null) {
|
||||
_search(_lastSearchedText!, _lastSearchFunction!);
|
||||
}
|
||||
}
|
||||
|
||||
void removeFilter() {
|
||||
_filteredList.clear();
|
||||
if (_lastSorting == null) {
|
||||
_controllerListener?.onListChanged(_list);
|
||||
} else {
|
||||
_sortedList.clear();
|
||||
_sortedList.addAll(List<T>.from(_list));
|
||||
_sortedList.sort(_lastSorting);
|
||||
_controllerListener?.onListChanged(_sortedList);
|
||||
}
|
||||
}
|
||||
|
||||
void removeSort() {
|
||||
_sortedList.clear();
|
||||
_lastSorting = null;
|
||||
_controllerListener
|
||||
?.onListChanged(_filteredList.isEmpty ? _list : _filteredList);
|
||||
}
|
||||
|
||||
void sortList(int Function(T a, T b) sorting) {
|
||||
_lastSorting = sorting;
|
||||
_sortedList.clear();
|
||||
_sortedList
|
||||
.addAll(List<T>.from(_filteredList.isEmpty ? _list : _filteredList));
|
||||
_sortedList.sort(sorting);
|
||||
_controllerListener?.onListChanged(_sortedList);
|
||||
}
|
||||
|
||||
void filterList(bool Function(T item) filter) {
|
||||
_filteredList.clear();
|
||||
_filteredList.addAll(_sortedList.isEmpty
|
||||
? _list.where(filter).toList()
|
||||
: _sortedList.where(filter).toList());
|
||||
_controllerListener?.onListChanged(_filteredList);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for a function that creates [ScaledTile] for a given index.
|
||||
typedef ScaledTile IndexedScaledTileBuilder(int index);
|
||||
|
||||
class SearchBar<T> extends StatefulWidget {
|
||||
/// Future returning searched items
|
||||
final Future<List<T>> Function(String text) onSearch;
|
||||
|
||||
/// List of items showed by default
|
||||
final List<T> suggestions;
|
||||
|
||||
/// Callback returning the widget corresponding to a Suggestion item
|
||||
final Widget Function(T item, int index)? buildSuggestion;
|
||||
|
||||
/// Minimum number of chars required for a search
|
||||
final int minimumChars;
|
||||
|
||||
/// Callback returning the widget corresponding to an item found
|
||||
final Widget Function(T item, int index) onItemFound;
|
||||
|
||||
/// Callback returning the widget corresponding to an Error while searching
|
||||
final Widget Function(Error error)? onError;
|
||||
|
||||
/// Cooldown between each call to avoid too many
|
||||
final Duration debounceDuration;
|
||||
|
||||
/// Widget to show when loading
|
||||
final Widget loader;
|
||||
|
||||
/// Widget to show when no item were found
|
||||
final Widget emptyWidget;
|
||||
|
||||
/// Widget to show by default
|
||||
final Widget? placeHolder;
|
||||
|
||||
/// Widget showed on left of the search bar
|
||||
final Widget icon;
|
||||
|
||||
/// Widget placed between the search bar and the results
|
||||
final Widget? header;
|
||||
|
||||
/// Hint text of the search bar
|
||||
final String hintText;
|
||||
|
||||
/// TextStyle of the hint text
|
||||
final TextStyle hintStyle;
|
||||
|
||||
/// Color of the icon when search bar is active
|
||||
final Color iconActiveColor;
|
||||
|
||||
/// Text style of the text in the search bar
|
||||
final TextStyle textStyle;
|
||||
|
||||
/// Widget shown for cancellation
|
||||
final Widget cancellationWidget;
|
||||
|
||||
/// Callback when cancel button is triggered
|
||||
final VoidCallback? onCancelled;
|
||||
|
||||
/// Controller used to be able to sort, filter or replay the search
|
||||
final SearchBarController? searchBarController;
|
||||
|
||||
/// Enable to edit the style of the search bar
|
||||
final SearchBarStyle searchBarStyle;
|
||||
|
||||
/// Number of items displayed on cross axis
|
||||
final int crossAxisCount;
|
||||
|
||||
/// Weather the list should take the minimum place or not
|
||||
final bool shrinkWrap;
|
||||
|
||||
/// Called to get the tile at the specified index for the
|
||||
/// [SliverGridStaggeredTileLayout].
|
||||
final IndexedScaledTileBuilder? indexedScaledTileBuilder;
|
||||
|
||||
/// Set the scrollDirection
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Spacing between tiles on main axis
|
||||
final double mainAxisSpacing;
|
||||
|
||||
/// Spacing between tiles on cross axis
|
||||
final double crossAxisSpacing;
|
||||
|
||||
/// Set a padding on the search bar
|
||||
final EdgeInsetsGeometry searchBarPadding;
|
||||
|
||||
/// Set a padding on the header
|
||||
final EdgeInsetsGeometry headerPadding;
|
||||
|
||||
/// Set a padding on the list
|
||||
final EdgeInsetsGeometry listPadding;
|
||||
|
||||
SearchBar({
|
||||
Key? key,
|
||||
required this.onSearch,
|
||||
required this.onItemFound,
|
||||
this.searchBarController,
|
||||
this.minimumChars = 3,
|
||||
this.debounceDuration = const Duration(milliseconds: 500),
|
||||
this.loader = const Center(child: CircularProgressIndicator()),
|
||||
this.onError,
|
||||
this.emptyWidget = const SizedBox.shrink(),
|
||||
this.header,
|
||||
this.placeHolder,
|
||||
this.icon = const Icon(Icons.search),
|
||||
this.hintText = "",
|
||||
this.hintStyle = const TextStyle(color: Color.fromRGBO(142, 142, 147, 1)),
|
||||
this.iconActiveColor = Colors.black,
|
||||
this.textStyle = const TextStyle(color: Colors.black),
|
||||
this.cancellationWidget = const Text("Cancel"),
|
||||
this.onCancelled,
|
||||
this.suggestions = const [],
|
||||
this.buildSuggestion,
|
||||
this.searchBarStyle = const SearchBarStyle(),
|
||||
this.crossAxisCount = 1,
|
||||
this.shrinkWrap = false,
|
||||
this.indexedScaledTileBuilder,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.mainAxisSpacing = 0.0,
|
||||
this.crossAxisSpacing = 0.0,
|
||||
this.listPadding = const EdgeInsets.all(0),
|
||||
this.searchBarPadding = const EdgeInsets.all(0),
|
||||
this.headerPadding = const EdgeInsets.all(0),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SearchBarState createState() => _SearchBarState<T>();
|
||||
}
|
||||
|
||||
class _SearchBarState<T> extends State<SearchBar<T>>
|
||||
with TickerProviderStateMixin, _ControllerListener<T> {
|
||||
bool _loading = false;
|
||||
Widget? _error;
|
||||
final _searchQueryController = TextEditingController();
|
||||
Timer? _debounce;
|
||||
bool _animate = false;
|
||||
List<T> _list = [];
|
||||
SearchBarController? searchBarController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
searchBarController =
|
||||
widget.searchBarController ?? SearchBarController<T>();
|
||||
searchBarController?.setListener(this);
|
||||
searchBarController?.setTextController(_searchQueryController, widget.minimumChars);
|
||||
}
|
||||
|
||||
@override
|
||||
void onListChanged(List<T> items) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_list = items;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onLoading() {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
_animate = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClear() {
|
||||
_cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(Error error) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = widget.onError != null ? widget.onError!(error) : Text("error");
|
||||
});
|
||||
}
|
||||
|
||||
_onTextChanged(String newText) async {
|
||||
if (_debounce?.isActive ?? false) {
|
||||
_debounce?.cancel();
|
||||
}
|
||||
|
||||
_debounce = Timer(widget.debounceDuration, () async {
|
||||
if (newText.length >= widget.minimumChars && widget.onSearch != null) {
|
||||
searchBarController?._search(newText, widget.onSearch);
|
||||
} else {
|
||||
setState(() {
|
||||
_list.clear();
|
||||
_error = null;
|
||||
_loading = false;
|
||||
_animate = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _cancel() {
|
||||
if (widget.onCancelled != null) {
|
||||
widget.onCancelled!();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_searchQueryController.clear();
|
||||
_list.clear();
|
||||
_error = null;
|
||||
_loading = false;
|
||||
_animate = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildListView(
|
||||
List<T> items, Widget Function(T item, int index) builder) {
|
||||
return Padding(
|
||||
padding: widget.listPadding,
|
||||
child: StaggeredGridView.countBuilder(
|
||||
crossAxisCount: widget.crossAxisCount,
|
||||
itemCount: items.length,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
staggeredTileBuilder:
|
||||
widget.indexedScaledTileBuilder ?? (int index) => ScaledTile.fit(1),
|
||||
scrollDirection: widget.scrollDirection,
|
||||
mainAxisSpacing: widget.mainAxisSpacing,
|
||||
crossAxisSpacing: widget.crossAxisSpacing,
|
||||
addAutomaticKeepAlives: true,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return builder(items[index], index);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildContent(BuildContext context) {
|
||||
if (_error != null) {
|
||||
return _error;
|
||||
} else if (_loading) {
|
||||
return widget.loader;
|
||||
} else if (_searchQueryController.text.length < widget.minimumChars) {
|
||||
if (widget.placeHolder != null) return widget.placeHolder;
|
||||
return _buildListView(
|
||||
widget.suggestions, widget.buildSuggestion ?? widget.onItemFound);
|
||||
} else if (_list.isNotEmpty) {
|
||||
return _buildListView(_list, widget.onItemFound);
|
||||
} else {
|
||||
return widget.emptyWidget;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final widthMax = MediaQuery.of(context).size.width;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: widget.searchBarPadding,
|
||||
child: Container(
|
||||
height: 80,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
width: _animate ? widthMax * .8 : widthMax,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: widget.searchBarStyle.borderRadius,
|
||||
color: widget.searchBarStyle.backgroundColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: widget.searchBarStyle.padding,
|
||||
child: Theme(
|
||||
child: TextField(
|
||||
controller: _searchQueryController,
|
||||
onChanged: _onTextChanged,
|
||||
style: widget.textStyle,
|
||||
decoration: InputDecoration(
|
||||
icon: widget.icon,
|
||||
border: InputBorder.none,
|
||||
hintText: widget.hintText,
|
||||
hintStyle: widget.hintStyle,
|
||||
),
|
||||
),
|
||||
data: Theme.of(context).copyWith(
|
||||
primaryColor: widget.iconActiveColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _cancel,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _animate ? 1.0 : 0,
|
||||
curve: Curves.easeIn,
|
||||
duration: Duration(milliseconds: _animate ? 1000 : 0),
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 200),
|
||||
width:
|
||||
_animate ? MediaQuery.of(context).size.width * .2 : 0,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: widget.cancellationWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: widget.headerPadding,
|
||||
child: widget.header ?? Container(),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildContent(context) ?? SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/vendor/flappy_search_bar/scaled_tile.dart
vendored
Normal file
31
lib/vendor/flappy_search_bar/scaled_tile.dart
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
/// Class only use in order to give access to StaggeredTile to users
|
||||
class ScaledTile extends StaggeredTile {
|
||||
/// Creates a [ScaledTile] with the given [crossAxisCellCount] that
|
||||
/// fit its main axis extent to its content.
|
||||
///
|
||||
/// This tile will have a fixed main axis extent.
|
||||
ScaledTile.fit(
|
||||
int crossAxisCellCount,
|
||||
) : super.fit(crossAxisCellCount);
|
||||
|
||||
/// Creates a [ScaledTile] with the given [crossAxisCellCount] and
|
||||
/// [mainAxisExtent].
|
||||
///
|
||||
/// This tile will have a fixed main axis extent.
|
||||
ScaledTile.extent(
|
||||
int crossAxisCellCount,
|
||||
double mainAxisExtent,
|
||||
) : super.extent(crossAxisCellCount, mainAxisExtent);
|
||||
|
||||
/// Creates a [ScaledTile] with the given [crossAxisCellCount] and
|
||||
/// [mainAxisCellCount].
|
||||
///
|
||||
/// The main axis extent of this tile will be the length of
|
||||
/// [mainAxisCellCount] cells (inner spacings included).
|
||||
ScaledTile.count(
|
||||
int crossAxisCellCount,
|
||||
num mainAxisCellCount,
|
||||
) : super.count(crossAxisCellCount, mainAxisCellCount as double);
|
||||
}
|
||||
12
lib/vendor/flappy_search_bar/search_bar_style.dart
vendored
Normal file
12
lib/vendor/flappy_search_bar/search_bar_style.dart
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchBarStyle {
|
||||
final Color backgroundColor;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
const SearchBarStyle(
|
||||
{this.backgroundColor = const Color.fromRGBO(142, 142, 147, .15),
|
||||
this.padding = const EdgeInsets.all(5.0),
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(5.0))});
|
||||
}
|
||||
20
lib/vendor/stripe_sdk/.github/workflows/publish.yaml
vendored
Normal file
20
lib/vendor/stripe_sdk/.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- workflow
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Dart and Flutter Package Publisher
|
||||
uses: k-paxian/dart-package-publisher@v1.2
|
||||
with:
|
||||
skiptests: false
|
||||
dryRunOnly: false
|
||||
credentialJson: ${{ secrets.CREDENTIAL_JSON }}
|
||||
140
lib/vendor/stripe_sdk/.gitignore
vendored
Normal file
140
lib/vendor/stripe_sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
|
||||
### JetBrains ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
.gradle/
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
74
lib/vendor/stripe_sdk/.gitignore.old
vendored
Normal file
74
lib/vendor/stripe_sdk/.gitignore.old
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
**/android/.gradle
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
10
lib/vendor/stripe_sdk/.metadata
vendored
Normal file
10
lib/vendor/stripe_sdk/.metadata
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
||||
290
lib/vendor/stripe_sdk/CHANGELOG.md
vendored
Normal file
290
lib/vendor/stripe_sdk/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
# Stripe SDK Changelog
|
||||
|
||||
## 5.0.0-nullsafety.1
|
||||
|
||||
## 4.0.2
|
||||
|
||||
* Fix a bug with date validation
|
||||
|
||||
## 4.0.1
|
||||
|
||||
* Fix assertion for webReturnPath
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Refactor, simplify and improve credit card validation in `StripeCard`
|
||||
* Add support for postal code in `AddPaymentMethodScreen` and `StripeCard`
|
||||
* Removed experimental status of `AddPaymentMethodScreen` and `PaymentMethodsScreen`
|
||||
* List up to 100 payment methods in `PaymentMethodsScreen`, up from 10
|
||||
* Add support for additional request parameters in `CustomerSession.listPaymentMethods`
|
||||
* Misc UI improvements on `AddPaymentMethodScreen`, e.g proper autofill hints and more.
|
||||
* Add new optional parameter `webReturnPath` to all SCA-capable methods. This allow specifying a custom return_url when compiled for web.
|
||||
* Fix issue with iOS not closing webview after SCA.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
This update removes many utility methods that were public, but not strictly related to Stripe or this library.
|
||||
In most cases this will not affect you, but if you relied on any of the removed methods I recommend you copy the methods
|
||||
into your project, or find other replacements.
|
||||
|
||||
* Removed many unused and unnecessary methods and constants on `StripeCard`
|
||||
* Removed file `card_utils.dart` and all it's contents.
|
||||
|
||||
## 3.2.0
|
||||
|
||||
* Add new method: `StripeApi.createSource()`
|
||||
|
||||
## 3.1.0
|
||||
|
||||
* `CustomerSession` now implements `ChangeNotifier`, notifying listeners when the session is ended.
|
||||
* `CustomerSession` will throw an error if attempted used after the session has ended.
|
||||
* Added `CustomerSession.endSession()` instance method.
|
||||
* `PaymentMethodStore` will now be cleared, listeners notified and finally disposed when the related `CustomerSession` is ended.
|
||||
|
||||
### Deprecated
|
||||
* `CustomerSession.endCustomerSession()` has been deprecated and replaced by `CustomerSession.instance.endSession`.
|
||||
|
||||
## 3.0.1+2
|
||||
|
||||
* Update dependencies
|
||||
* Fix misc warnings and lint issues
|
||||
|
||||
## 3.0.1+1
|
||||
|
||||
* Fix lint issues
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Fix unhandled exception due to unknown parameter: exp_mont
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Added Support for custom decorations in card form
|
||||
* Updated default stripe api version to `2020-03-02`
|
||||
|
||||
### Breaking changes
|
||||
* Removed `Card.toMap()`
|
||||
* Removed support for non-card properties on Card (billing details)
|
||||
* See https://github.com/ezet/stripe-sdk/issues/61#issuecomment-662657924
|
||||
* Rename `confirmSetupIntentWithPaymentMethod` to `confirmSetupIntent` as this is the default, general case.
|
||||
* Rename `confirmSetupIntent` to `authenticateSetupIntent` so it aligns more with `authenticatePaymentIntent`, as they share similar behavior.
|
||||
* Make `returnUrlForSca` a required parameter for Stripe
|
||||
|
||||
#### Removed following deprecated functions and classes
|
||||
* class: CardNumberFormatter
|
||||
* constructor: CustomerSession
|
||||
* Stripe.getReturnUrl
|
||||
* Stripe.handlePaymentIntent
|
||||
* Stripe.handleSetupIntent
|
||||
|
||||
## 2.8.2
|
||||
|
||||
* Updated readme
|
||||
* Updated uni_links
|
||||
* Added additional API documentation
|
||||
* Fixed validation for expiration month
|
||||
|
||||
## 2.8.1
|
||||
|
||||
Please provide feedback on experimental features and examples.
|
||||
|
||||
* Fix misc minor bugs
|
||||
* Fix misc lint issues
|
||||
|
||||
### Experimental: PaymentMethodsScreen
|
||||
|
||||
* Major improvements to UI and functionality
|
||||
* Use a shared cache to avoid loading all payment methods repeatedly
|
||||
* Create setup intent immediately upon entering add PM screen
|
||||
|
||||
### Experimental: CheckoutScreen
|
||||
|
||||
* Basic implementation of a checkout screen
|
||||
|
||||
### Example
|
||||
|
||||
* Misc changes related to PaymentMethodsScreen
|
||||
* Started example implementation of CheckoutScreen
|
||||
|
||||
## 2.8.0
|
||||
* Expose StripeApiException and StripeApiError
|
||||
* Deprecated Stripe.handlePaymentIntent. Contact me if you used this.
|
||||
* Deprecated Stripe.handleSetupIntent. Contact me if you used this.
|
||||
* Deprecated `CustomerSession()` constructor. 3.0 will enforce the singleton pattern.
|
||||
|
||||
* Removed experimental parameter `nextAction` from `Stripe.authenticatePayment()`
|
||||
* Added `Stripe.authenticatePaymentWithNextAction()`
|
||||
|
||||
### Experimental: PaymentMethodsScreen
|
||||
A complete UI screen that lets a user view, delete and add stripe payment methods.
|
||||
|
||||
* Slide right to display delete option
|
||||
* Press `+` to open `AddPaymentMethodScreen`
|
||||
|
||||
### Example
|
||||
|
||||
* Re-organize example code
|
||||
|
||||
## 2.7.0
|
||||
|
||||
* Use explicit type for CreateSetupIntent return.
|
||||
* Added optional [nextAction] parameter to Stripe.authenticatePayment.
|
||||
* Make [paymentMethodId] of `Stripe.confirmPayment` optional
|
||||
|
||||
* Fix bug which prevented ephemeral keys from refreshing correctly.
|
||||
|
||||
### Example
|
||||
* Add [Customer Details]
|
||||
* Add [Payments] with automatic and manual confirmation
|
||||
|
||||
## 2.6.0
|
||||
|
||||
* Add visual card widget in CardForm
|
||||
|
||||
* Fix misc bugs in CardForm
|
||||
|
||||
## 2.5.3
|
||||
|
||||
* Add focus handling for CardForm.
|
||||
* Give card number focus by default.
|
||||
* Enable tapping "next/arrow" on keyboard to move to next field.
|
||||
|
||||
## 2.5.2
|
||||
|
||||
* Added a complete demo application, available in /examples/app.
|
||||
* Display, add and remove payment methods.
|
||||
* Add payment methods with and without Setup Intent.
|
||||
* Quickly test pre-made Stripe test cards, with SCA and more.
|
||||
|
||||
* Made the `StripeApi` instance on `Stripe` objects public. This avoids having to create
|
||||
a separate StripeApi instance.
|
||||
|
||||
* Fixed bug which prevented ephemeral keys from refreshing correctly.
|
||||
|
||||
### Experimental:
|
||||
|
||||
* Added "AddPaymentMethod" screen, which handles the complete flow of adding a payment card.
|
||||
* This is still WIP and in beta stage, meaning the API might change.
|
||||
|
||||
## 2.5.1
|
||||
|
||||
Minor breaking change.
|
||||
|
||||
* Rename returnUrlForSCA to returnUrlForSca.
|
||||
|
||||
|
||||
## 2.5.0
|
||||
|
||||
* Add support for custom return url scheme
|
||||
* Update documentation
|
||||
|
||||
## 2.4.5
|
||||
|
||||
* Add links to complete examples of the stripe SDK and supporting backend.
|
||||
|
||||
## 2.4.4
|
||||
|
||||
* Fix bug which caused initial expiry date not to be set
|
||||
* Add runnable example in example/app
|
||||
|
||||
## 2.4.3
|
||||
|
||||
* Add support for custom text style on card form and form fields
|
||||
* Added examples for card form and form fields
|
||||
|
||||
## 2.4.2
|
||||
|
||||
* Allow custom form field error text
|
||||
* Fix bug where card number input field would allow more than 16 digits
|
||||
|
||||
|
||||
## 2.4.1
|
||||
|
||||
* Add support for custom input decorators on card form and form fields
|
||||
|
||||
## 2.4.0
|
||||
|
||||
* Minor breaking change: Split package into two separate sub-libraries:
|
||||
* stripe_sdk: API related functionality
|
||||
* stripe_sdk_ui: UI widgets and utilities
|
||||
|
||||
* Replaced ListView with Column inside the CardForm
|
||||
* Updated dependencies
|
||||
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
* Add CardForm widget, which can be used to add or edit credit cards.
|
||||
* Complete validation for card number, expiration date and CVC
|
||||
* Individual FormField widgets can be used to create a custom form
|
||||
* Fix bug in Stripe.authenticatePayment
|
||||
|
||||
## 2.2.0
|
||||
|
||||
* Add support for connected accounts
|
||||
* Add optional constructor parameter `stripeAccount` for all APIs
|
||||
|
||||
## 2.1.1
|
||||
|
||||
* Remove unused stripeAccount property on `StripeApi`
|
||||
|
||||
## 2.1.0
|
||||
|
||||
* Add Stripe.confirmSetupIntentWithPaymentMethod
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Rewrite of internal API
|
||||
* Fixed several issues and bugs
|
||||
* Restructured public API with breaking changes
|
||||
* Split `Stripe` into `StripeApi` and `Stripe`
|
||||
* SCA related features moved to `Stripe`
|
||||
* Moved basic Stripe API requests to `StripeApi`
|
||||
|
||||
See README and examples for further details details.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
* Misc minor updates and fixes
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Complete support for SetupIntent with SCA
|
||||
* Fix bug with confirmPayment
|
||||
* Internal refactoring
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Support multiple simultaneous authentication flows
|
||||
* Improve documentation and examples
|
||||
* Allow specifying apiVersion for CustomerSession
|
||||
* Allow multiple instances of Stripe and CustomerSession
|
||||
|
||||
## 1.0.0+1
|
||||
|
||||
* Improve examples
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Improve API for SCA-related features
|
||||
* Improve examples
|
||||
|
||||
## 0.0.2
|
||||
|
||||
* Remove typed models
|
||||
* Add some support for payment intents
|
||||
* Complete support for payment methods
|
||||
* Complete support for tokens
|
||||
* Add examples
|
||||
* Major cleanup
|
||||
|
||||
## 0.0.1+2
|
||||
|
||||
* Add analysis_options.yaml
|
||||
* Fix dartanalyzer issues
|
||||
* Fix other misc packaging issues
|
||||
|
||||
## 0.0.1+1
|
||||
|
||||
* Initial release
|
||||
26
lib/vendor/stripe_sdk/LICENSE
vendored
Normal file
26
lib/vendor/stripe_sdk/LICENSE
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2020, Lars-Kristian Dahl
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those
|
||||
of the authors and should not be interpreted as representing official policies,
|
||||
either expressed or implied, of the Stripe SDK project.
|
||||
247
lib/vendor/stripe_sdk/README.md
vendored
Normal file
247
lib/vendor/stripe_sdk/README.md
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
[](https://pub.dev/packages/stripe_sdk)
|
||||
|
||||
# Flutter Stripe SDK
|
||||
|
||||
A native dart package for Stripe. There are various other flutter plugins that wrap existing Stripe libraries,
|
||||
but this package uses a different approach.
|
||||
It does not wrap existing Stripe libraries, but instead accesses the Stripe API directly.
|
||||
|
||||
Flutter support:
|
||||
* [x] iOS
|
||||
* [x] Android
|
||||
* [x] Web
|
||||
|
||||
See *example/main.dart* for additional short examples.
|
||||
|
||||
See <https://github.com/ezet/stripe-sdk/tree/master/example> for a complete demo application,
|
||||
with a working example backend.
|
||||
|
||||
Demo backend: <https://github.com/ezet/stripe-sdk-demo-api>
|
||||
|
||||
## Features
|
||||
|
||||
* Supports all types of SCA, including 3DS, 3DS2, BankID and others.
|
||||
* Handle payments with complete SCA support.
|
||||
* Add, remove and update payment methods, sources and cards, optionally with SCA.
|
||||
* Manage customer information.
|
||||
* Create all types of Stripe tokens.
|
||||
* Forms, widgets and utilities to use directly, or create your own UI!
|
||||
|
||||
### Experimental
|
||||
|
||||
* Managed UI flow for adding payment methods with SCA (using SetupIntent).
|
||||
|
||||
### Supported APIs
|
||||
|
||||
- PaymentIntent, with SCA
|
||||
- SetupIntent, with SCA
|
||||
- PaymentMethod
|
||||
- Customer
|
||||
- Cards
|
||||
- Sources
|
||||
- Tokens
|
||||
|
||||
### Planned features
|
||||
|
||||
- Offer managed UI flow for checkout
|
||||
|
||||
## Demo application
|
||||
|
||||
There is a complete demo application available at <https://github.com/ezet/stripe-sdk/tree/master/example/app>.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/ezet/stripe-sdk/master/doc/demo.png" width="300">
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
- The return type for each function is `Future<Map<String, dynamic>>`, where the value depends on the stripe API version.
|
||||
|
||||
The library has three classes to access the Stripe API:
|
||||
|
||||
- `Stripe` for generic, non-customer specific APIs, using publishable keys.
|
||||
- `CustomerSession` for customer-specific APIs, using stripe ephemeral keys.
|
||||
- `StripeApi` enables raw REST calls against the Stripe API.
|
||||
|
||||
|
||||
### Stripe
|
||||
|
||||
- <https://stripe.dev/stripe-android/index.html?com/stripe/android/Stripe.html>
|
||||
|
||||
Aims to provide high-level functionality similar to the official mobile Stripe SDKs.
|
||||
|
||||
### CustomerSession
|
||||
|
||||
_Requires a Stripe ephemeral key._
|
||||
|
||||
- <https://stripe.com/docs/mobile/android/customer-information#customer-session-no-ui>
|
||||
- <https://stripe.com/docs/mobile/android/standard#creating-ephemeral-keys>
|
||||
|
||||
Provides functionality similar to CustomerSession in the Stripe Android SDK.
|
||||
|
||||
### StripeApi
|
||||
|
||||
- <https://stripe.com/docs/api>
|
||||
|
||||
Provides basic low-level methods to access the Stripe REST API.
|
||||
|
||||
- Limited to the APIs that can be used with a public key or ephemeral key.
|
||||
- Library methods map to a Stripe API call with the same name.
|
||||
- Additional parameters can be provided as an optional argument.
|
||||
|
||||
|
||||
_`Stripe` and `CustomerSession` use this internally._
|
||||
|
||||
## Initialization
|
||||
|
||||
All classes offer a singleton instance that can be initiated by calling the `init(...)` methods and then accessed through `.instance`.
|
||||
Regular instances can also be created using the constructor, which allows them to be managed by e.g. dependency injection instead.
|
||||
|
||||
### Stripe
|
||||
|
||||
```dart
|
||||
Stripe.init('pk_xxx');
|
||||
// or, to manage your own instance, or multiple instances
|
||||
final stripe = Stripe('pk_xxx');
|
||||
```
|
||||
|
||||
### CustomerSession
|
||||
|
||||
The function that retrieves the ephemeral key must return the JSON response as a plain string.
|
||||
|
||||
```dart
|
||||
CustomerSession.init((apiVersion) => server.getEphemeralKeyFromServer(apiVersion));
|
||||
// or, to manage your own instances
|
||||
final session = CustomerSession((apiVersion) => server.getEphemeralKeyFromServer(apiVersion));
|
||||
```
|
||||
|
||||
### StripeApi
|
||||
|
||||
```dart
|
||||
StripeApi.init('pk_xxx');
|
||||
// or, to manage your own instances
|
||||
final stripeApi = StripeApi('pk_xxx');
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
Use `CardForm` to add or edit credit card details, or build your own form using the pre-built FormFields.
|
||||
|
||||
```dart
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final card = StripeCard();
|
||||
|
||||
final form = CardForm(card: card, formKey: formKey);
|
||||
|
||||
onPressed: () {
|
||||
if (formKey.currentState.validate()) {
|
||||
formKey.currentState.save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
<img src="https://raw.githubusercontent.com/ezet/stripe-sdk/master/doc/cardform.png" width="300">
|
||||
|
||||
|
||||
|
||||
## SCA/PSD2
|
||||
|
||||
The library offers complete support for SCA on iOS and Android.
|
||||
It handles all types of SCA, including 3DS, 3DS2, BankID and others.
|
||||
It handles SCA by launching the authentication flow in a web browser, and returns the result to the app.
|
||||
The `returnUrlForSca` parameter must match the configuration of your `AndroidManifest.xml` and `Info.plist` as shown in the next steps.
|
||||
|
||||
```dart
|
||||
Stripe.init('pk_xxx', returnUrlForSca: 'stripesdk://3ds.stripesdk.io');
|
||||
final clientSecret = await server.createPaymentIntent(Stripe.instance.getReturnUrlForSca());
|
||||
final paymentIntent = await Stripe.instance.confirmPayment(clientSecret, paymentMethodId: 'pm_card_visa');
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
You need to declare the following intent filter in `android/app/src/main/AndroidManifest.xml`.
|
||||
This example is for the url `stripesdk://3ds.stripesdk.io`:
|
||||
|
||||
```xml
|
||||
<manifest ...>
|
||||
<!-- ... other tags -->
|
||||
<application ...>
|
||||
<activity ...>
|
||||
|
||||
<!-- The launchMode should be singleTop or singleTask,
|
||||
to avoid launching a new instance of the app when SCA has been completed. -->
|
||||
android:launchMode="singleTop"
|
||||
|
||||
|
||||
<!-- ... other tags -->
|
||||
|
||||
<!-- Deep Links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="stripesdk"
|
||||
android:host="3ds.stripesdk.io" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### IOS
|
||||
|
||||
For iOS you need to declare the scheme in `ios/Runner/Info.plist` (or through Xcode's Target Info editor,
|
||||
under URL Types). This example is for the url `stripesdk://3ds.stripesdk.io`:
|
||||
|
||||
```xml
|
||||
<!-- ... other tags -->
|
||||
<plist>
|
||||
<dict>
|
||||
<!-- ... other tags -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>3ds.stripesdk.io</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>stripesdk</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- ... other tags -->
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
|
||||
## Experimental
|
||||
|
||||
Experimental features are marked as `deprecated` and the API is subject to change until it is deemed stable.
|
||||
Feel free to use these features but be aware that breaking changes might be introduced in minor updates.
|
||||
|
||||
### Add Payment Method
|
||||
|
||||
Use `AddPaymentMethodScreen.withSetupIntent(...)` to launch a managed UI flow for adding a payment method.
|
||||
This will also handle SCA if required.
|
||||
|
||||
### Payment Methods
|
||||
|
||||
`PaymentMethodsScreen` offers a prebuilt UI that can:
|
||||
|
||||
* List all current payment methods
|
||||
* Add new payment methods, using setup intents
|
||||
* Delete existing payment methods
|
||||
|
||||
## Additional examples
|
||||
|
||||
### Glappen
|
||||
This is a complete application, with a mobile client and a backend API.
|
||||
Documentation is lacking, but it can serve as an example for more advanced use.
|
||||
|
||||
App: <https://github.com/ezet/glappen-client>
|
||||
Backend: <https://github.com/ezet/glappen-firebase-api>
|
||||
16
lib/vendor/stripe_sdk/analysis_options.yaml
vendored
Normal file
16
lib/vendor/stripe_sdk/analysis_options.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
# Uncomment to specify additional rules.
|
||||
#linter:
|
||||
# rules:
|
||||
# prefer_single_quotes: true
|
||||
# deprecated_member_use_from_same_package: false
|
||||
# missing_required_param: false
|
||||
|
||||
analyzer:
|
||||
# errors:
|
||||
# omit_local_variable_types: ignore
|
||||
strong-mode:
|
||||
implicit-casts: true
|
||||
implicit-dynamic: true
|
||||
148
lib/vendor/stripe_sdk/lib/src/customer_session.dart
vendored
Normal file
148
lib/vendor/stripe_sdk/lib/src/customer_session.dart
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'ephemeral_key_manager.dart';
|
||||
import 'stripe_api_handler.dart';
|
||||
|
||||
class CustomerSession extends ChangeNotifier {
|
||||
static const int keyRefreshBufferInSeconds = 30;
|
||||
|
||||
static CustomerSession? _instance;
|
||||
|
||||
final StripeApiHandler _apiHandler;
|
||||
|
||||
final EphemeralKeyManager _keyManager;
|
||||
final String apiVersion;
|
||||
|
||||
bool isDisposed = false;
|
||||
|
||||
/// Create a new CustomerSession instance. Use this if you prefer to manage your own instances.
|
||||
CustomerSession._(EphemeralKeyProvider provider, {this.apiVersion = defaultApiVersion, String? stripeAccount})
|
||||
: _keyManager = EphemeralKeyManager(provider, keyRefreshBufferInSeconds),
|
||||
_apiHandler = StripeApiHandler(stripeAccount: stripeAccount) {
|
||||
_apiHandler.apiVersion = apiVersion;
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
/// Initiate the customer session singleton instance.
|
||||
/// If [prefetchKey] is true, fetch the ephemeral key immediately.
|
||||
static void initCustomerSession(EphemeralKeyProvider provider,
|
||||
{String apiVersion = defaultApiVersion, String? stripeAccount, bool prefetchKey = true}) {
|
||||
CustomerSession._(provider, apiVersion: apiVersion, stripeAccount: stripeAccount);
|
||||
if (prefetchKey) {
|
||||
_instance!._keyManager.retrieveEphemeralKey();
|
||||
}
|
||||
}
|
||||
|
||||
/// End the managed singleton customer session.
|
||||
/// Call this when the current user logs out.
|
||||
@Deprecated('Use CustomerSession.instance.endSession instead.')
|
||||
static void endCustomerSession() {
|
||||
_instance?.endSession();
|
||||
}
|
||||
|
||||
/// End the managed singleton customer session.
|
||||
/// Call this when the current user logs out.
|
||||
void endSession() {
|
||||
notifyListeners();
|
||||
dispose();
|
||||
isDisposed = true;
|
||||
if (identical(_instance, this)) {
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current customer session
|
||||
static CustomerSession get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception(
|
||||
'Attempted to get instance of CustomerSession before initialization. Please initialize a new session using [CustomerSession.initCustomerSession() first.]');
|
||||
}
|
||||
assert(_instance!._assertNotDisposed());
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Retrieves the details for the current customer.
|
||||
/// https://stripe.com/docs/api/customers/retrieve
|
||||
Future<Map<String, dynamic>> retrieveCurrentCustomer() async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/customers/${key.customerId}';
|
||||
return _apiHandler.request(RequestMethod.get, path, key.secret, apiVersion);
|
||||
}
|
||||
|
||||
/// List a Customer's PaymentMethods.
|
||||
/// https://stripe.com/docs/api/payment_methods/list
|
||||
Future<Map<String, dynamic>> listPaymentMethods(
|
||||
{type = 'card', int? limit, String? endingBefore, String? startingAfter}) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
const path = '/payment_methods';
|
||||
final params = {'customer': key.customerId, 'type': type};
|
||||
if (limit != null) params['limit'] = limit;
|
||||
if (startingAfter != null) params['starting_after'] = startingAfter;
|
||||
if (endingBefore != null) params['ending_before'] = endingBefore;
|
||||
return _apiHandler.request(RequestMethod.get, path, key.secret, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Attach a PaymentMethod.
|
||||
/// https://stripe.com/docs/api/payment_methods/attach
|
||||
Future<Map<String, dynamic>> attachPaymentMethod(String? paymentMethodId) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/payment_methods/$paymentMethodId/attach';
|
||||
final params = {'customer': key.customerId};
|
||||
return _apiHandler.request(RequestMethod.post, path, key.secret, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Detach a PaymentMethod.
|
||||
/// https://stripe.com/docs/api/payment_methods/detach
|
||||
Future<Map<String, dynamic>> detachPaymentMethod(String? paymentMethodId) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/payment_methods/$paymentMethodId/detach';
|
||||
return _apiHandler.request(RequestMethod.post, path, key.secret, apiVersion);
|
||||
}
|
||||
|
||||
/// Attaches a Source object to the Customer.
|
||||
/// The source must be in a chargeable or pending state.
|
||||
/// https://stripe.com/docs/api/sources/attach
|
||||
Future<Map<String, dynamic>> attachSource(String sourceId) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/customers/${key.customerId}/sources';
|
||||
final params = {'source': sourceId};
|
||||
return _apiHandler.request(RequestMethod.post, path, key.secret, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Detaches a Source object from a Customer.
|
||||
/// The status of a source is changed to consumed when it is detached and it can no longer be used to create a charge.
|
||||
/// https://stripe.com/docs/api/sources/detach
|
||||
Future<Map<String, dynamic>> detachSource(String sourceId) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/customers/${key.customerId}/sources/$sourceId';
|
||||
return _apiHandler.request(RequestMethod.delete, path, key.secret, apiVersion);
|
||||
}
|
||||
|
||||
/// Updates the specified customer by setting the values of the parameters passed.
|
||||
/// https://stripe.com/docs/api/customers/update
|
||||
Future<Map<String, dynamic>> updateCustomer(Map<String, dynamic> data) async {
|
||||
assert(_assertNotDisposed());
|
||||
final EphemeralKey key = await (_keyManager.retrieveEphemeralKey());
|
||||
final path = '/customers/${key.customerId}';
|
||||
return _apiHandler.request(RequestMethod.post, path, key.secret, apiVersion, params: data);
|
||||
}
|
||||
|
||||
bool _assertNotDisposed() {
|
||||
assert(() {
|
||||
if (isDisposed) {
|
||||
throw FlutterError('A $runtimeType was used after being disposed.\n'
|
||||
'Once you have called dispose() on a $runtimeType, it can no longer be used.');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
119
lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart
vendored
Normal file
119
lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show json;
|
||||
import 'dart:developer';
|
||||
|
||||
import 'stripe_api_handler.dart';
|
||||
import 'stripe_error.dart';
|
||||
import 'util/stripe_json_utils.dart';
|
||||
|
||||
/// Function that takes a apiVersion and returns a Stripe ephemeral key response
|
||||
typedef EphemeralKeyProvider = Future<String> Function(String apiVersion);
|
||||
|
||||
/// Represents a Stripe Ephemeral Key
|
||||
class EphemeralKey {
|
||||
static const String fieldCreated = 'created';
|
||||
static const String fieldExpires = 'expires';
|
||||
static const String fieldSecret = 'secret';
|
||||
static const String fieldLiveMode = 'livemode';
|
||||
static const String fieldObject = 'object';
|
||||
static const String fieldId = 'id';
|
||||
static const String fieldAssociatedObjects = 'associated_objects';
|
||||
static const String fieldType = 'type';
|
||||
|
||||
static const String fieldNull = 'null';
|
||||
|
||||
late String _id;
|
||||
late int _created;
|
||||
late int _expires;
|
||||
late bool _liveMode;
|
||||
late String _customerId;
|
||||
late String _object;
|
||||
late String _secret;
|
||||
late String _type;
|
||||
late DateTime _createdAt;
|
||||
late DateTime _expiresAt;
|
||||
|
||||
EphemeralKey.fromJson(Map<String, dynamic> json) {
|
||||
// TODO might throw an error if ephemeralKey doesn't provide all fields.
|
||||
_id = optString(json, fieldId)!;
|
||||
_created = optInteger(json, fieldCreated)!;
|
||||
_expires = optInteger(json, fieldExpires)!;
|
||||
_liveMode = optBoolean(json, fieldLiveMode)!;
|
||||
_customerId = json[fieldAssociatedObjects][0][fieldId];
|
||||
_type = json[fieldAssociatedObjects][0][fieldType];
|
||||
_object = optString(json, fieldObject)!;
|
||||
_secret = optString(json, fieldSecret)!;
|
||||
_createdAt = DateTime.fromMillisecondsSinceEpoch(_created * 1000);
|
||||
_expiresAt = DateTime.fromMillisecondsSinceEpoch(_expires * 1000);
|
||||
}
|
||||
|
||||
String get id => _id;
|
||||
|
||||
int get created => _created;
|
||||
|
||||
int get expires => _expires;
|
||||
|
||||
String get customerId => _customerId;
|
||||
|
||||
bool get liveMode => _liveMode;
|
||||
|
||||
String get object => _object;
|
||||
|
||||
String get secret => _secret;
|
||||
|
||||
String get type => _type;
|
||||
|
||||
DateTime get createdAt => _createdAt;
|
||||
|
||||
DateTime get expiresAt => _expiresAt;
|
||||
}
|
||||
|
||||
class EphemeralKeyManager {
|
||||
EphemeralKey? _ephemeralKey;
|
||||
final EphemeralKeyProvider ephemeralKeyProvider;
|
||||
final int timeBufferInSeconds;
|
||||
|
||||
EphemeralKeyManager(this.ephemeralKeyProvider, this.timeBufferInSeconds);
|
||||
|
||||
/// Retrieve a ephemeral key.
|
||||
/// Will fetch a new one using [EphemeralKeyProvider] if required.
|
||||
Future<EphemeralKey> retrieveEphemeralKey() async {
|
||||
if (_shouldRefreshKey()) {
|
||||
String key;
|
||||
try {
|
||||
key = await ephemeralKeyProvider(defaultApiVersion);
|
||||
} catch (error) {
|
||||
log(error.toString());
|
||||
rethrow;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, dynamic> decodedKey = json.decode(key);
|
||||
_ephemeralKey = EphemeralKey.fromJson(decodedKey);
|
||||
} catch (error) {
|
||||
log(error.toString());
|
||||
final e = StripeApiError(null, {
|
||||
StripeApiError.fieldMessage:
|
||||
'Failed to parse Ephemeral Key, Please return the response as it is as you received from stripe server',
|
||||
});
|
||||
throw StripeApiException(e);
|
||||
}
|
||||
|
||||
return _ephemeralKey!;
|
||||
} else {
|
||||
return _ephemeralKey!;
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldRefreshKey() {
|
||||
if (_ephemeralKey == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final diff = _ephemeralKey!.expiresAt.difference(now);
|
||||
return diff.inSeconds < timeBufferInSeconds;
|
||||
}
|
||||
}
|
||||
93
lib/vendor/stripe_sdk/lib/src/models/card.dart
vendored
Normal file
93
lib/vendor/stripe_sdk/lib/src/models/card.dart
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:credit_card_validator/credit_card_validator.dart';
|
||||
|
||||
|
||||
class StripeCard {
|
||||
final _ccValidator = CreditCardValidator();
|
||||
|
||||
String? number;
|
||||
String? cvc;
|
||||
int? expMonth;
|
||||
int? expYear;
|
||||
String? last4;
|
||||
String? postalCode;
|
||||
|
||||
StripeCard({
|
||||
this.number,
|
||||
this.cvc,
|
||||
this.expMonth,
|
||||
this.expYear,
|
||||
this.last4,
|
||||
});
|
||||
|
||||
/// Checks whether or not the {@link #number} field is valid.
|
||||
///
|
||||
/// @return {@code true} if valid, {@code false} otherwise.
|
||||
bool isPostalCodeValid() {
|
||||
return (postalCode != null && postalCode!.isNotEmpty)
|
||||
? int.tryParse(postalCode!) != null
|
||||
: false;
|
||||
}
|
||||
|
||||
/// Checks whether or not the {@link #number} field is valid.
|
||||
///
|
||||
/// @return {@code true} if valid, {@code false} otherwise.
|
||||
bool validateNumber() {
|
||||
return number != null ? _ccValidator.validateCCNum(number!).isValid : false;
|
||||
}
|
||||
|
||||
/// Checks whether or not the {@link #expMonth} and {@link #expYear} fields represent a valid
|
||||
/// expiry date.
|
||||
///
|
||||
/// @return {@code true} if valid, {@code false} otherwise
|
||||
bool validateDate() {
|
||||
return _ccValidator
|
||||
.validateExpDate(
|
||||
'${expMonth.toString().padLeft(2, '0')}/${expYear.toString().padLeft(2, '0')}')
|
||||
.isValid;
|
||||
}
|
||||
|
||||
/// Checks whether or not the {@link #cvc} field is valid.
|
||||
///
|
||||
/// @return {@code true} if valid, {@code false} otherwise
|
||||
bool validateCVC() {
|
||||
if (cvc == null) return false;
|
||||
return _ccValidator.validateCVV(cvc!, _ccValidator.validateCCNum(number!).ccType).isValid;
|
||||
}
|
||||
|
||||
/// Returns a stripe hash that represents this card.
|
||||
/// It only sets the type and card details. In order to add additional details such as name and address,
|
||||
/// you need to insert these keys into the hash before submitting it.
|
||||
Map<String, Object> toPaymentMethod() {
|
||||
final Map<String, Object> map = {
|
||||
'type': 'card',
|
||||
'card': {
|
||||
'number': number,
|
||||
'cvc': cvc,
|
||||
'exp_month': expMonth,
|
||||
'exp_year': expYear,
|
||||
},
|
||||
'billing_details': {
|
||||
'address': {'postal_code': postalCode}
|
||||
}
|
||||
};
|
||||
_removeNullAndEmptyParams(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
static void _removeNullAndEmptyParams(Map<String, Object?> mapToEdit) {
|
||||
// Remove all null values; they cause validation errors
|
||||
final List<String> keys = mapToEdit.keys.toList(growable: false);
|
||||
for (String key in keys) {
|
||||
final Object? value = mapToEdit[key];
|
||||
if (value == null) {
|
||||
mapToEdit.remove(key);
|
||||
} else if (value is String) {
|
||||
if (value.isEmpty) {
|
||||
mapToEdit.remove(key);
|
||||
}
|
||||
} else if (value is Map) {
|
||||
_removeNullAndEmptyParams(value as Map<String, Object?>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
268
lib/vendor/stripe_sdk/lib/src/stripe.dart
vendored
Normal file
268
lib/vendor/stripe_sdk/lib/src/stripe.dart
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'ui/stripe_web_view.dart';
|
||||
import "package:universal_html/html.dart" as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stripe_api.dart';
|
||||
import 'ui/stripe_ui.dart';
|
||||
|
||||
class Stripe {
|
||||
/// Creates a new [Stripe] object. Use this constructor if you wish to handle the instance of this class by yourself.
|
||||
/// Alternatively, use [Stripe.init] to create a singleton and access it through [Stripe.instance].
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "pk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
///
|
||||
/// [returnUrlForSca] should be used to specify a unique return url for
|
||||
/// Strong Customer Authentication (SCA) such as 3DS, 3DS2, BankID and others.
|
||||
/// It is required to use your own app specific url scheme and host. This
|
||||
/// parameter must match your "android/app/src/main/AndroidManifest.xml"
|
||||
/// and "ios/Runner/Info.plist" configuration.
|
||||
Stripe(String publishableKey, {String? stripeAccount})
|
||||
: api = StripeApi(publishableKey, stripeAccount: stripeAccount);
|
||||
|
||||
final StripeApi api;
|
||||
static Stripe? _instance;
|
||||
|
||||
/// Access the instance of Stripe by calling [Stripe.instance].
|
||||
/// Throws an [Exception] if [Stripe.init] hasn't been called previously.
|
||||
static Stripe get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('Attempted to get singleton instance of Stripe without initialization');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Initializes the singleton instance of [Stripe]. Afterwards you can
|
||||
/// use [Stripe.instance] to access the created instance.
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "pk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
///
|
||||
/// [returnUrlForSca] should be used to specify a unique return url for
|
||||
/// Strong Customer Authentication (SCA) such as 3DS, 3DS2, BankID and others.
|
||||
/// It is required to use your own app specific url scheme and host. This
|
||||
/// parameter must match your "android/app/src/main/AndroidManifest.xml"
|
||||
/// and "ios/Runner/Info.plist" configuration.
|
||||
static void init(String publishableKey, {String? stripeAccount}) {
|
||||
_instance = Stripe(publishableKey, stripeAccount: stripeAccount);
|
||||
StripeApi.init(publishableKey, stripeAccount: stripeAccount);
|
||||
}
|
||||
|
||||
/// Creates a return URL that can be used to authenticate a single PaymentIntent.
|
||||
/// This should be set on the intent before attempting to authenticate it.
|
||||
String getReturnUrlForSca({String? webReturnUrl}) {
|
||||
if (kIsWeb) {
|
||||
return webReturnUrl ?? StripeUiOptions.defaultWebReturnUrl;
|
||||
} else {
|
||||
final requestId = Random.secure().nextInt(99999999);
|
||||
return '${StripeUiOptions.defaultMobileReturnUrl}?requestId=$requestId';
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticate a SetupIntent
|
||||
/// https://stripe.com/docs/api/setup_intents/confirm
|
||||
Future<Map<String, dynamic>> authenticateSetupIntent(String clientSecret,
|
||||
{String? webReturnPath, required BuildContext context}) async {
|
||||
final Map<String, dynamic> intent = await api.confirmSetupIntent(
|
||||
clientSecret,
|
||||
data: {'return_url': getReturnUrlForSca(webReturnUrl: webReturnPath)},
|
||||
);
|
||||
return _handleSetupIntent(intent, context);
|
||||
}
|
||||
|
||||
/// Confirm and authenticate a SetupIntent
|
||||
/// https://stripe.com/docs/api/setup_intents/confirm
|
||||
Future<Map<String, dynamic>> confirmSetupIntent(String clientSecret, String paymentMethod,
|
||||
{String? webReturnPath, required BuildContext context}) async {
|
||||
var returnUrlForSca = getReturnUrlForSca(webReturnUrl: webReturnPath);
|
||||
final Map<String, dynamic> intent = await api.confirmSetupIntent(
|
||||
clientSecret,
|
||||
data: {
|
||||
'return_url': returnUrlForSca,
|
||||
'payment_method': paymentMethod,
|
||||
},
|
||||
);
|
||||
return _handleSetupIntent(intent, context);
|
||||
}
|
||||
|
||||
/// Confirm and authenticate a payment.
|
||||
/// Returns the PaymentIntent.
|
||||
/// https://stripe.com/docs/payments/payment-intents/android
|
||||
Future<Map<String, dynamic>> confirmPayment(String paymentIntentClientSecret, BuildContext context,
|
||||
{String? paymentMethodId}) async {
|
||||
final data = {'return_url': getReturnUrlForSca()};
|
||||
if (paymentMethodId != null) data['payment_method'] = paymentMethodId;
|
||||
final Map<String, dynamic> paymentIntent = await api.confirmPaymentIntent(
|
||||
paymentIntentClientSecret,
|
||||
data: data,
|
||||
);
|
||||
return _handlePaymentIntent(paymentIntent, context);
|
||||
}
|
||||
|
||||
/// Confirm and authenticate a payment with Google Pay.
|
||||
/// /// [paymentResult] must be the result of requesting a Google Pay payment with the `pay` library.
|
||||
/// Returns the PaymentIntent.
|
||||
Future<Map<String, dynamic>> confirmPaymentWithGooglePay(BuildContext context,
|
||||
{required String paymentIntentClientSecret, required Map<String, dynamic> paymentResult}) async {
|
||||
final data = <String, dynamic>{'return_url': getReturnUrlForSca()};
|
||||
final String token = paymentResult['paymentMethodData']['tokenizationData']['token'] as String;
|
||||
final tokenJson = jsonDecode(token) as Map<String, dynamic>;
|
||||
final tokenId = tokenJson['id'] as String;
|
||||
data['payment_method_data'] = {
|
||||
'type': 'card',
|
||||
"card": {"token": tokenId}
|
||||
};
|
||||
final Map<String, dynamic> paymentIntent = await api.confirmPaymentIntent(
|
||||
paymentIntentClientSecret,
|
||||
data: data,
|
||||
);
|
||||
return _handlePaymentIntent(paymentIntent, context);
|
||||
}
|
||||
|
||||
/// Confirm and authenticate a payment with Apple Pay.
|
||||
/// [paymentResult] must be the result of requesting a Apple Pay paymet with the `pay` library.
|
||||
/// Returns the PaymentIntent.
|
||||
Future<Map<String, dynamic>> confirmPaymentWithApplePay(BuildContext context,
|
||||
{required String paymentIntentClientSecret, required Map<String, dynamic> paymentResult}) async {
|
||||
final tokenPayload = <String, dynamic>{};
|
||||
tokenPayload['pk_token'] = paymentResult['token'];
|
||||
final paymentMethod = _getPaymentMethod(paymentResult['paymentMethod']);
|
||||
tokenPayload['pk_token_instrument_name'] = paymentMethod['displayName'];
|
||||
tokenPayload['pk_token_payment_network'] = paymentMethod['network'];
|
||||
tokenPayload['card'] = <String, dynamic>{};
|
||||
tokenPayload['pk_token_transaction_id'] = paymentResult['transactionIdentifier'];
|
||||
|
||||
if (tokenPayload['pk_token_transaction_id'] == "Simulated Identifier") {
|
||||
tokenPayload['pk_token_transaction_id'] =
|
||||
"ApplePayStubs~4242424242424242~200~USD~${DateTime.now().millisecondsSinceEpoch}";
|
||||
}
|
||||
|
||||
final stripeToken = await api.createToken(tokenPayload);
|
||||
final tokenId = stripeToken['id'] as String;
|
||||
|
||||
final data = <String, dynamic>{'return_url': getReturnUrlForSca()};
|
||||
data['payment_method_data'] = {
|
||||
'type': 'card',
|
||||
"card": {"token": tokenId}
|
||||
};
|
||||
final Map<String, dynamic> paymentIntent = await api.confirmPaymentIntent(
|
||||
paymentIntentClientSecret,
|
||||
data: data,
|
||||
);
|
||||
return _handlePaymentIntent(paymentIntent, context);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getPaymentMethod(dynamic paymentMethod) {
|
||||
if (paymentMethod is String) {
|
||||
return jsonDecode(paymentMethod) as Map<String, dynamic>;
|
||||
} else {
|
||||
return paymentMethod as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
|
||||
/// Authenticate a payment.
|
||||
/// Returns the PaymentIntent.
|
||||
/// https://stripe.com/docs/payments/payment-intents/android-manual
|
||||
Future<Map<String, dynamic>> authenticatePayment(String paymentIntentClientSecret, BuildContext context) async {
|
||||
final Map<String, dynamic> paymentIntent = await api.retrievePaymentIntent(paymentIntentClientSecret);
|
||||
return _handlePaymentIntent(paymentIntent, context);
|
||||
}
|
||||
|
||||
/// Authenticate a payment with [paymentIntent].
|
||||
/// This is similar to [authenticatePayment] but is slightly more efficient,
|
||||
/// as it avoids the request to the Stripe API to retrieve the action.
|
||||
/// To use this, return the complete [paymentIntent] from your server.
|
||||
Future<Map<String, dynamic>> _handlePaymentIntent(Map<String, dynamic> paymentIntent, BuildContext context) async {
|
||||
return _authenticateIntent(paymentIntent, context, api.retrievePaymentIntent);
|
||||
}
|
||||
|
||||
/// Launch 3DS in a new browser window.
|
||||
/// Returns a [Future] with the Stripe SetupIntent when the user completes or cancels authentication.
|
||||
Future<Map<String, dynamic>> _handleSetupIntent(Map<String, dynamic> setupIntent, BuildContext context) async {
|
||||
return _authenticateIntent(setupIntent, context, api.retrieveSetupIntent);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _authenticateIntent(Map<String, dynamic> intent, BuildContext context,
|
||||
Future<Map<String, dynamic>> Function(String clientSecret) getIntentFunction) async {
|
||||
if (intent['status'] != 'requires_action') return intent;
|
||||
final clientSecret = intent['client_secret'];
|
||||
final action = intent['next_action'];
|
||||
final String url = action['redirect_to_url']['url'];
|
||||
final returnUri = Uri.parse(action['redirect_to_url']['return_url']);
|
||||
|
||||
if (kIsWeb) {
|
||||
return _authenticateWithBrowser(context, url, returnUri, getIntentFunction, clientSecret);
|
||||
} else {
|
||||
await _authenticateWithWebView(context, url, returnUri);
|
||||
return getIntentFunction(clientSecret);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _authenticateWithBrowser(BuildContext context, String url, Uri returnUri,
|
||||
Future<Map<String, dynamic>> Function(String clientSecret) getIntentFunction, String clientSecret) async {
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
|
||||
late StreamSubscription<html.Event> subscription;
|
||||
subscription = html.window.onFocus.listen((event) async {
|
||||
final intent = await getIntentFunction(clientSecret);
|
||||
if (intent['status'] != 'requires_action') {
|
||||
Navigator.of(context).pop();
|
||||
subscription.cancel();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
completer.complete(intent);
|
||||
return;
|
||||
}
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: const Text("Awaiting authentication, please complete authentication in the opened window."),
|
||||
children: [
|
||||
SimpleDialogOption(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
subscription.cancel();
|
||||
completer.complete(getIntentFunction(clientSecret));
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: const Text("Open new window"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
launchUrl(Uri.parse(url));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await launchUrl(Uri.parse(url));
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<bool?> _authenticateWithWebView(BuildContext context, String url, Uri returnUri) async {
|
||||
return Navigator.push<bool?>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StripeWebView(
|
||||
uri: url,
|
||||
returnUri: returnUri,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
139
lib/vendor/stripe_sdk/lib/src/stripe_api.dart
vendored
Normal file
139
lib/vendor/stripe_sdk/lib/src/stripe_api.dart
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'models/card.dart';
|
||||
import 'stripe_api_handler.dart';
|
||||
|
||||
typedef IntentProvider = Future<Map<String, dynamic>> Function(Uri uri);
|
||||
|
||||
class StripeApi {
|
||||
static StripeApi? _instance;
|
||||
|
||||
final StripeApiHandler _apiHandler;
|
||||
|
||||
final String publishableKey;
|
||||
|
||||
final String apiVersion;
|
||||
|
||||
/// Create a new instance, which can be used with e.g. dependency injection.
|
||||
/// Throws a [Exception] if an invalid [publishableKey] has been submitted.
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "pk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
StripeApi(this.publishableKey, {this.apiVersion = defaultApiVersion, String? stripeAccount})
|
||||
: _apiHandler = StripeApiHandler(stripeAccount: stripeAccount) {
|
||||
_validateKey(publishableKey);
|
||||
_apiHandler.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
/// Initialize the managed singleton instance of [StripeApi].
|
||||
/// Afterwards you can use [StripeApi.instance] to access the created instance.
|
||||
///
|
||||
/// [publishableKey] is your publishable key, beginning with "pk_".
|
||||
/// Your can copy your key from https://dashboard.stripe.com/account/apikeys
|
||||
///
|
||||
/// [stripeAccount] is the id of a stripe customer and stats with "cus_".
|
||||
/// This is a optional parameter.
|
||||
static void init(String publishableKey, {String apiVersion = defaultApiVersion, String? stripeAccount}) {
|
||||
_instance ??= StripeApi(publishableKey, apiVersion: apiVersion, stripeAccount: stripeAccount);
|
||||
}
|
||||
|
||||
/// Access the singleton instance of [StripeApi].
|
||||
/// Throws an [Exception] if [StripeApi.init] hasn't been called previously.
|
||||
static StripeApi get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('Attempted to get singleton instance of StripeApi without initialization');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Create a stripe Token
|
||||
/// https://stripe.com/docs/api/tokens
|
||||
Future<Map<String, dynamic>> createToken(Map<String, dynamic> data) async {
|
||||
const path = '/tokens';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Create a PaymentMethod.
|
||||
/// https://stripe.com/docs/api/payment_methods/create
|
||||
Future<Map<String, dynamic>> createPaymentMethod(Map<String, dynamic> data) async {
|
||||
const path = '/payment_methods';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Create a PaymentMethod from a card.
|
||||
/// This will only create a PaymentMethod with the minimum required properties.
|
||||
/// To include additional properties such as billing details, use [StripeCard.toPaymentMethod], add additional details
|
||||
/// and then use [createPaymentMethod].
|
||||
Future<Map<String, dynamic>> createPaymentMethodFromCard(StripeCard card) async {
|
||||
return createPaymentMethod(card.toPaymentMethod());
|
||||
}
|
||||
|
||||
/// Create a new Source object.
|
||||
/// https://stripe.com/docs/api/sources/create
|
||||
Future<Map<String, dynamic>> createSource(Map<String, dynamic> data) async {
|
||||
const path = '/sources';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Retrieve a PaymentIntent.
|
||||
/// https://stripe.com/docs/api/payment_intents/retrieve
|
||||
Future<Map<String, dynamic>> retrievePaymentIntent(String clientSecret, {String? apiVersion}) async {
|
||||
final intentId = _parseIdFromClientSecret(clientSecret);
|
||||
final path = '/payment_intents/$intentId';
|
||||
final params = {'client_secret': clientSecret};
|
||||
return _apiHandler.request(RequestMethod.get, path, publishableKey, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Confirm a PaymentIntent
|
||||
/// https://stripe.com/docs/api/payment_intents/confirm
|
||||
Future<Map<String, dynamic>> confirmPaymentIntent(String clientSecret, {Map<String, dynamic>? data}) async {
|
||||
data ??= {};
|
||||
final intent = _parseIdFromClientSecret(clientSecret);
|
||||
data['client_secret'] = clientSecret;
|
||||
final path = '/payment_intents/$intent/confirm';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Retrieve a SetupIntent.
|
||||
/// https://stripe.com/docs/api/setup_intents/retrieve
|
||||
Future<Map<String, dynamic>> retrieveSetupIntent(String clientSecret, {String? apiVersion}) async {
|
||||
final intentId = _parseIdFromClientSecret(clientSecret);
|
||||
final path = '/setup_intents/$intentId';
|
||||
final params = {'client_secret': clientSecret};
|
||||
return _apiHandler.request(RequestMethod.get, path, publishableKey, apiVersion, params: params);
|
||||
}
|
||||
|
||||
/// Confirm a SetupIntent
|
||||
/// https://stripe.com/docs/api/setup_intents/confirm
|
||||
Future<Map<String, dynamic>> confirmSetupIntent(String clientSecret, {Map<String, dynamic>? data}) async {
|
||||
data ??= {};
|
||||
final intent = _parseIdFromClientSecret(clientSecret);
|
||||
data['client_secret'] = clientSecret;
|
||||
final path = '/setup_intents/$intent/confirm';
|
||||
return _apiHandler.request(RequestMethod.post, path, publishableKey, apiVersion, params: data);
|
||||
}
|
||||
|
||||
/// Validates the received [publishableKey] and throws a [Exception] if an
|
||||
/// invalid key has been submitted.
|
||||
static void _validateKey(String publishableKey) {
|
||||
if (publishableKey.isEmpty) {
|
||||
throw Exception('Invalid Publishable Key: '
|
||||
'You must use a valid publishable key to create a token. '
|
||||
'For more info, see https://stripe.com/docs/stripe.js.');
|
||||
}
|
||||
|
||||
if (publishableKey.startsWith('sk_')) {
|
||||
throw Exception('Invalid Publishable Key: '
|
||||
'You are using a secret key to create a token, '
|
||||
'instead of the publishable one. For more info, '
|
||||
'see https://stripe.com/docs/stripe.js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _parseIdFromClientSecret(String clientSecret) {
|
||||
return clientSecret.split('_secret')[0];
|
||||
}
|
||||
185
lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart
vendored
Normal file
185
lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show json;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'stripe_error.dart';
|
||||
|
||||
const String defaultApiVersion = '2020-03-02';
|
||||
|
||||
enum RequestMethod { get, post, put, delete, option }
|
||||
|
||||
class StripeApiHandler {
|
||||
static const String liveApiBase = 'https://api.stripe.com';
|
||||
static const String liveLogginBase = 'https://q.stripe.com';
|
||||
static const String liveLoggingEndpoint = 'https://m.stripe.com/4';
|
||||
static const String liveApiPath = liveApiBase + '/v1';
|
||||
|
||||
static const String charset = 'UTF-8';
|
||||
static const String customers = 'customers';
|
||||
static const String tokens = 'tokens';
|
||||
static const String sources = 'sources';
|
||||
|
||||
static const String headerKeyRequestId = 'Request-Id';
|
||||
static const String fieldError = 'error';
|
||||
static const String fieldSource = 'source';
|
||||
|
||||
String apiVersion = defaultApiVersion;
|
||||
|
||||
static const String malformedResponseMessage = 'An improperly formatted error response was found.';
|
||||
|
||||
final http.Client _client = http.Client();
|
||||
|
||||
final String? stripeAccount;
|
||||
|
||||
StripeApiHandler({this.stripeAccount});
|
||||
|
||||
Future<Map<String, dynamic>> request(RequestMethod method, String path, String key, String? apiVersion,
|
||||
{final Map<String, dynamic>? params}) {
|
||||
final options = RequestOptions(key: key, apiVersion: apiVersion, stripeAccount: stripeAccount);
|
||||
return _getStripeResponse(method, liveApiPath + path, options, params: params);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getStripeResponse(RequestMethod method, final String url, final RequestOptions options,
|
||||
{final Map<String, dynamic>? params}) async {
|
||||
final Map<String, String> headers = _headers(options: options);
|
||||
|
||||
http.Response response;
|
||||
|
||||
switch (method) {
|
||||
case RequestMethod.get:
|
||||
var fUrl = url;
|
||||
if (params != null && params.isNotEmpty) {
|
||||
fUrl = '$url?${_encodeMap(params)}';
|
||||
}
|
||||
response = await _client.get(Uri.parse(fUrl), headers: headers);
|
||||
break;
|
||||
|
||||
case RequestMethod.post:
|
||||
response = await _client.post(
|
||||
Uri.parse(url),
|
||||
headers: headers,
|
||||
body: params != null ? _urlEncodeMap(params) : null,
|
||||
);
|
||||
break;
|
||||
|
||||
case RequestMethod.delete:
|
||||
response = await _client.delete(Uri.parse(url), headers: headers);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Request Method: $method not implemented');
|
||||
}
|
||||
|
||||
final requestId = response.headers[headerKeyRequestId];
|
||||
|
||||
final statusCode = response.statusCode;
|
||||
late Map<String, dynamic> resp;
|
||||
try {
|
||||
resp = json.decode(response.body);
|
||||
} catch (error) {
|
||||
final stripeError = StripeApiError(requestId, {StripeApiError.fieldMessage: malformedResponseMessage});
|
||||
throw StripeApiException(stripeError);
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
final Map<String, dynamic> errBody = resp[fieldError];
|
||||
final stripeError = StripeApiError(requestId, errBody);
|
||||
throw StripeApiException(stripeError);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
static Map<String, String> _headers({RequestOptions? options}) {
|
||||
final Map<String, String> headers = {};
|
||||
headers['Accept-Charset'] = charset;
|
||||
headers['Accept'] = 'application/json';
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
headers['User-Agent'] = 'StripeSDK/v2';
|
||||
|
||||
if (options != null) {
|
||||
headers['Authorization'] = 'Bearer ${options.key}';
|
||||
|
||||
if (options.apiVersion != null) {
|
||||
headers['Stripe-Version'] = options.apiVersion!;
|
||||
}
|
||||
|
||||
if (options.stripeAccount != null) {
|
||||
headers['Stripe-Account'] = options.stripeAccount!;
|
||||
}
|
||||
|
||||
if (options.idempotencyKey != null) {
|
||||
headers['Idempotency-Key'] = options.idempotencyKey!;
|
||||
}
|
||||
}
|
||||
|
||||
// debug headers
|
||||
final propertyMap = <String, String>{};
|
||||
propertyMap['os.name'] = defaultTargetPlatform.toString();
|
||||
propertyMap['lang'] = 'Dart';
|
||||
propertyMap['publisher'] = 'lars.dahl@gmail.com';
|
||||
headers['X-Stripe-Client-User-Agent'] = json.encode(propertyMap);
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
static String _encodeMap(Map<String, dynamic> params) {
|
||||
return params.keys
|
||||
.map((key) => '${Uri.encodeComponent(key)}=${Uri.encodeComponent(params[key].toString())}')
|
||||
.join('&');
|
||||
}
|
||||
|
||||
static String _urlEncodeMap(dynamic data) {
|
||||
final urlData = StringBuffer();
|
||||
var first = true;
|
||||
void urlEncode(dynamic sub, String path) {
|
||||
if (sub is List) {
|
||||
for (var i = 0; i < sub.length; i++) {
|
||||
urlEncode(sub[i], '$path%5B%5D');
|
||||
}
|
||||
} else if (sub is Map) {
|
||||
sub.forEach((k, v) {
|
||||
if (path == '') {
|
||||
urlEncode(v, Uri.encodeQueryComponent(k));
|
||||
} else {
|
||||
urlEncode(v, '$path%5B${Uri.encodeQueryComponent(k)}%5D');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!first) {
|
||||
urlData.write('&');
|
||||
}
|
||||
first = false;
|
||||
urlData.write('$path=${Uri.encodeQueryComponent(sub.toString())}');
|
||||
}
|
||||
}
|
||||
|
||||
urlEncode(data, '');
|
||||
return urlData.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class RequestOptions {
|
||||
static const String typeQuery = 'source';
|
||||
static const String typeJson = 'json_data';
|
||||
|
||||
final String? apiVersion; // TODO might have no usage
|
||||
final String? guid;
|
||||
final String? idempotencyKey;
|
||||
final String key;
|
||||
final String? requestType; // TODO might have no usage
|
||||
final String? stripeAccount;
|
||||
|
||||
RequestOptions({
|
||||
required this.apiVersion,
|
||||
this.guid,
|
||||
this.idempotencyKey,
|
||||
required this.key,
|
||||
this.requestType,
|
||||
this.stripeAccount,
|
||||
});
|
||||
}
|
||||
67
lib/vendor/stripe_sdk/lib/src/stripe_error.dart
vendored
Normal file
67
lib/vendor/stripe_sdk/lib/src/stripe_error.dart
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'util/stripe_json_utils.dart';
|
||||
|
||||
class StripeApiError {
|
||||
static const String fieldType = 'type';
|
||||
static const String fieldCharge = 'charge';
|
||||
static const String fieldCode = 'code';
|
||||
static const String fieldDeclineCode = 'decline_code';
|
||||
static const String fieldDocUrl = 'doc_url';
|
||||
static const String fieldMessage = 'message';
|
||||
static const String fieldParam = 'param';
|
||||
|
||||
final String? requestId;
|
||||
final String? type;
|
||||
final String? charge;
|
||||
final String? code;
|
||||
final String? declineCode;
|
||||
final String? docUrl;
|
||||
final String? message;
|
||||
final String? param;
|
||||
|
||||
StripeApiError._internal(
|
||||
this.requestId,
|
||||
this.type,
|
||||
this.charge,
|
||||
this.code,
|
||||
this.declineCode,
|
||||
this.docUrl,
|
||||
this.message,
|
||||
this.param,
|
||||
);
|
||||
|
||||
factory StripeApiError(String? requestId, Map<String, dynamic> json) {
|
||||
final type = optString(json, fieldType);
|
||||
final charge = optString(json, fieldCharge);
|
||||
final code = optString(json, fieldCode);
|
||||
final declineCode = optString(json, fieldDeclineCode);
|
||||
final docUrl = optString(json, fieldDocUrl);
|
||||
final message = optString(json, fieldMessage);
|
||||
final param = optString(json, fieldParam);
|
||||
|
||||
return StripeApiError._internal(
|
||||
requestId,
|
||||
type,
|
||||
charge,
|
||||
code,
|
||||
declineCode,
|
||||
docUrl,
|
||||
message,
|
||||
param,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StripeApiException implements Exception {
|
||||
final StripeApiError error;
|
||||
final String? requestId;
|
||||
final String message;
|
||||
|
||||
StripeApiException(this.error)
|
||||
: requestId = error.requestId,
|
||||
message = error.message!;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
121
lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart
vendored
Normal file
121
lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../stripe_error.dart';
|
||||
import '../ui/stripe_ui.dart';
|
||||
|
||||
import '../stripe.dart';
|
||||
import 'models.dart';
|
||||
import 'progress_bar.dart';
|
||||
import 'stores/payment_method_store.dart';
|
||||
import 'widgets/payment_method_selector.dart';
|
||||
|
||||
class CheckoutPage extends StatefulWidget {
|
||||
final Future<IntentClientSecret> Function() createPaymentIntent;
|
||||
final void Function(BuildContext context, Map<String, dynamic> paymentIntent) onPaymentSuccess;
|
||||
final void Function(BuildContext context, Map<String, dynamic> paymentIntent) onPaymentFailed;
|
||||
final void Function(BuildContext context, StripeApiException e) onPaymentError;
|
||||
|
||||
CheckoutPage({
|
||||
Key? key,
|
||||
required this.createPaymentIntent,
|
||||
void Function(BuildContext context, Map<String, dynamic> paymentIntent)? onPaymentSuccess,
|
||||
void Function(BuildContext context, Map<String, dynamic> paymentIntent)? onPaymentFailed,
|
||||
void Function(BuildContext, StripeApiException)? onPaymentError,
|
||||
}) : onPaymentSuccess = onPaymentSuccess ?? StripeUiOptions.onPaymentSuccess,
|
||||
onPaymentFailed = onPaymentFailed ?? StripeUiOptions.onPaymentFailed,
|
||||
onPaymentError = onPaymentError ?? StripeUiOptions.onPaymentError,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_CheckoutPageState createState() => _CheckoutPageState();
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
class _CheckoutPageState extends State<CheckoutPage> {
|
||||
final PaymentMethodStore paymentMethodStore = PaymentMethodStore.instance;
|
||||
|
||||
String? _selectedPaymentMethod;
|
||||
late Future<IntentClientSecret> _clientSecretFuture;
|
||||
late Future<Map<String, dynamic>> paymentIntentFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_clientSecretFuture = widget.createPaymentIntent();
|
||||
paymentIntentFuture =
|
||||
_clientSecretFuture.then((value) => Stripe.instance.api.retrievePaymentIntent(value.clientSecret));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: PaymentMethodSelector(
|
||||
paymentMethodStore: paymentMethodStore,
|
||||
initialPaymentMethodId: null,
|
||||
onChanged: (value) => setState(() {
|
||||
_selectedPaymentMethod = value;
|
||||
debugPrint("Selected paymentMethod changed: $value");
|
||||
})),
|
||||
),
|
||||
FutureBuilder<Map<String, dynamic>>(
|
||||
future: paymentIntentFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
debugPrint(snapshot.error.toString());
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StripeUiOptions.payButtonBuilder(
|
||||
context,
|
||||
snapshot.data!,
|
||||
_selectedPaymentMethod == null
|
||||
? null
|
||||
: _createAttemptPaymentFunction(context, _clientSecretFuture)),
|
||||
);
|
||||
} else {
|
||||
return _selectedPaymentMethod == null
|
||||
? const SizedBox.shrink()
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void Function() _createAttemptPaymentFunction(BuildContext context, Future<IntentClientSecret> paymentIntentFuture) {
|
||||
return () async {
|
||||
showProgressDialog(context);
|
||||
final initialPaymentIntent = await paymentIntentFuture.catchError((error){
|
||||
hideProgressDialog(context);
|
||||
});
|
||||
try {
|
||||
final confirmedPaymentIntent = await Stripe.instance
|
||||
.confirmPayment(initialPaymentIntent.clientSecret, context, paymentMethodId: _selectedPaymentMethod);
|
||||
if (confirmedPaymentIntent['status'] == 'succeeded') {
|
||||
widget.onPaymentSuccess(context, confirmedPaymentIntent);
|
||||
hideProgressDialog(context);
|
||||
} else {
|
||||
widget.onPaymentFailed(context, confirmedPaymentIntent);
|
||||
hideProgressDialog(context);
|
||||
}
|
||||
} catch (e) {
|
||||
hideProgressDialog(context);
|
||||
debugPrint(e.toString());
|
||||
if (e is StripeApiException) {
|
||||
widget.onPaymentError(context, e);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
6
lib/vendor/stripe_sdk/lib/src/ui/models.dart
vendored
Normal file
6
lib/vendor/stripe_sdk/lib/src/ui/models.dart
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class IntentClientSecret {
|
||||
final String clientSecret;
|
||||
final String? status;
|
||||
|
||||
IntentClientSecret(this.status, this.clientSecret);
|
||||
}
|
||||
12
lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart
vendored
Normal file
12
lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void hideProgressDialog(BuildContext context) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
Future<bool?> showProgressDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
144
lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart
vendored
Normal file
144
lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../stores/payment_method_store.dart';
|
||||
|
||||
import '../../models/card.dart';
|
||||
import '../../stripe.dart';
|
||||
import '../models.dart';
|
||||
import '../progress_bar.dart';
|
||||
import '../stripe_ui.dart';
|
||||
import '../widgets/card_form.dart';
|
||||
|
||||
/// A screen that collects, creates and attaches a payment method to a stripe customer.
|
||||
///
|
||||
/// Payment methods can be created with and without a Setup Intent. Using a Setup Intent is highly recommended.
|
||||
///
|
||||
class AddPaymentMethodScreen extends StatefulWidget {
|
||||
final Stripe stripe;
|
||||
|
||||
/// Used to create a setup intent when required.
|
||||
final createSetupIntent = StripeUiOptions.createSetupIntent;
|
||||
|
||||
/// The payment method store used to manage payment methods.
|
||||
final PaymentMethodStore paymentMethodStore;
|
||||
|
||||
/// Custom Title for the screen
|
||||
final String title;
|
||||
static const String _defaultTitle = 'Add payment method';
|
||||
|
||||
static Route<String?> route(
|
||||
{PaymentMethodStore? paymentMethodStore, Stripe? stripe, CardForm? form, String title = _defaultTitle}) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => AddPaymentMethodScreen(
|
||||
paymentMethodStore: paymentMethodStore ?? PaymentMethodStore.instance,
|
||||
stripe: stripe ?? Stripe.instance,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Add a payment method using a Stripe Setup Intent
|
||||
AddPaymentMethodScreen({Key? key, required this.paymentMethodStore, required this.stripe, this.title = _defaultTitle})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_AddPaymentMethodScreenState createState() => _AddPaymentMethodScreenState();
|
||||
}
|
||||
|
||||
class _AddPaymentMethodScreenState extends State<AddPaymentMethodScreen> {
|
||||
Future<IntentClientSecret>? setupIntentFuture;
|
||||
final form = CardForm();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.createSetupIntent != null) setupIntentFuture = widget.createSetupIntent!();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () async {
|
||||
final formState = form.formKey.currentState;
|
||||
if (formState?.validate() ?? false) {
|
||||
formState!.save();
|
||||
await _tryCreatePaymentMethod(context, form.card);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
form,
|
||||
if (StripeUiOptions.showTestPaymentMethods)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
_createTestCardButton("4242424242424242"),
|
||||
_createTestCardButton("4000000000003220"),
|
||||
_createTestCardButton("4000000000003063"),
|
||||
_createTestCardButton("4000008400001629"),
|
||||
_createTestCardButton("4000008400001280"),
|
||||
_createTestCardButton("4000000000003055"),
|
||||
_createTestCardButton("4000000000003097"),
|
||||
_createTestCardButton("378282246310005"),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _createTestCardButton(String number) {
|
||||
return OutlinedButton(
|
||||
child: Text(number.substring(number.length - 4)),
|
||||
onPressed: () =>
|
||||
_tryCreatePaymentMethod(context, StripeCard(number: number, cvc: "123", expMonth: 1, expYear: 2030)));
|
||||
}
|
||||
|
||||
Future<void> _tryCreatePaymentMethod(BuildContext context, StripeCard cardData) async {
|
||||
FocusManager.instance.primaryFocus!.unfocus();
|
||||
showProgressDialog(context);
|
||||
try {
|
||||
await _createPaymentMethod(cardData, context);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createPaymentMethod(StripeCard cardData, BuildContext context) async {
|
||||
var paymentMethod = await widget.stripe.api.createPaymentMethodFromCard(cardData);
|
||||
if (setupIntentFuture != null) {
|
||||
final initialSetupIntent =
|
||||
await setupIntentFuture!.timeout(const Duration(seconds: 10)).whenComplete(() => hideProgressDialog(context));
|
||||
final confirmedSetupIntent = await widget.stripe
|
||||
.confirmSetupIntent(initialSetupIntent.clientSecret, paymentMethod['id'], context: context);
|
||||
|
||||
if (confirmedSetupIntent['status'] == 'succeeded') {
|
||||
/// A new payment method has been attached, so refresh the store.
|
||||
await widget.paymentMethodStore.refresh();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text("Payment method successfully added."),
|
||||
));
|
||||
Navigator.pop(context, paymentMethod['id']);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Authentication failed, please try again.")));
|
||||
}
|
||||
} else {
|
||||
paymentMethod = await (widget.paymentMethodStore.attachPaymentMethod(paymentMethod['id']))
|
||||
.whenComplete(() => hideProgressDialog(context));
|
||||
Navigator.pop(context, paymentMethod['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
177
lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart
vendored
Normal file
177
lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../stripe.dart';
|
||||
import '../../ui/stores/payment_method_store.dart';
|
||||
import '../progress_bar.dart';
|
||||
import 'add_payment_method_screen.dart';
|
||||
|
||||
class PaymentMethodsScreen extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
/// The payment method store to use.
|
||||
final PaymentMethodStore _paymentMethodStore;
|
||||
|
||||
static Route<void> route({String title = '', PaymentMethodStore? paymentMethodStore}) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => PaymentMethodsScreen(
|
||||
title: title,
|
||||
paymentMethodStore: paymentMethodStore,
|
||||
));
|
||||
}
|
||||
|
||||
PaymentMethodsScreen({Key? key, this.title = 'Payment Methods', PaymentMethodStore? paymentMethodStore})
|
||||
: _paymentMethodStore = paymentMethodStore ?? PaymentMethodStore.instance,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stripe = Stripe.instance;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context, AddPaymentMethodScreen.route(paymentMethodStore: _paymentMethodStore, stripe: stripe));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: PaymentMethodsList(
|
||||
paymentMethodStore: _paymentMethodStore,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentMethod {
|
||||
final String id;
|
||||
final String last4;
|
||||
final String brand;
|
||||
final DateTime expirationDate;
|
||||
|
||||
const PaymentMethod(this.id, this.last4, this.brand, this.expirationDate);
|
||||
|
||||
String getExpirationAsString() {
|
||||
return '${expirationDate.month}/${expirationDate.year}';
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is PaymentMethod) return id == other.id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentMethodsList extends StatefulWidget {
|
||||
final PaymentMethodStore paymentMethodStore;
|
||||
|
||||
const PaymentMethodsList({Key? key, required this.paymentMethodStore}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PaymentMethodsListState createState() => _PaymentMethodsListState();
|
||||
}
|
||||
|
||||
class _PaymentMethodsListState extends State<PaymentMethodsList> {
|
||||
List<PaymentMethod> paymentMethods = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.paymentMethodStore.addListener(paymentMethodStoreListener);
|
||||
setState(() => paymentMethods = widget.paymentMethodStore.paymentMethods);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PaymentMethodsList oldWidget) {
|
||||
widget.paymentMethodStore.addListener(paymentMethodStoreListener);
|
||||
widget.paymentMethodStore.removeListener(paymentMethodStoreListener);
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.paymentMethodStore.removeListener(paymentMethodStoreListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void paymentMethodStoreListener() {
|
||||
if (mounted) {
|
||||
setState(() => paymentMethods = widget.paymentMethodStore.paymentMethods);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await widget.paymentMethodStore.refresh(),
|
||||
child: buildListView(paymentMethods, widget.paymentMethodStore, context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildListView(List<PaymentMethod> listData, PaymentMethodStore paymentMethods, BuildContext rootContext) {
|
||||
if (paymentMethods.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return ListView.builder(
|
||||
itemCount: listData.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final card = listData[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onLongPress: () async {},
|
||||
onTap: () => _displayDeletePaymentMethodDialog(rootContext, card, paymentMethods),
|
||||
title: Text(card.last4),
|
||||
subtitle: Text(card.brand.toUpperCase()),
|
||||
leading: const Icon(Icons.credit_card),
|
||||
// trailing: card.id == defaultPaymentMethod.paymentMethodId ? Icon(Icons.check_circle) : null,
|
||||
trailing: IconButton(
|
||||
onPressed: () => _displayDeletePaymentMethodDialog(rootContext, card, paymentMethods),
|
||||
icon: const Icon(Icons.cancel),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _displayDeletePaymentMethodDialog(
|
||||
BuildContext context, PaymentMethod card, PaymentMethodStore paymentMethods) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext alertDialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Delete payment method'),
|
||||
content: const Text('Are you sure you want to delete this payment method?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(alertDialogContext, rootNavigator: true).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(alertDialogContext, rootNavigator: true).pop();
|
||||
showProgressDialog(context);
|
||||
await widget.paymentMethodStore
|
||||
.detachPaymentMethod(card.id)
|
||||
.whenComplete(() => hideProgressDialog(context));
|
||||
ScaffoldMessenger.of(context)
|
||||
..clearSnackBars()
|
||||
..showSnackBar(const SnackBar(
|
||||
content: Text('Payment method successfully deleted.'),
|
||||
));
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
89
lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart
vendored
Normal file
89
lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../customer_session.dart';
|
||||
import '../screens/payment_methods_screen.dart';
|
||||
|
||||
/// A managed repository for payment methods.
|
||||
/// This is the preferred way to work with payment methods when using Flutter.
|
||||
/// The store will only refresh itself if there are active listeners.
|
||||
class PaymentMethodStore extends ChangeNotifier {
|
||||
final List<PaymentMethod> paymentMethods = [];
|
||||
bool isLoading = false;
|
||||
bool isInitialized = false;
|
||||
|
||||
/// The customer session the store operates on.
|
||||
final CustomerSession _customerSession;
|
||||
|
||||
static PaymentMethodStore? _instance;
|
||||
|
||||
/// Access the singleton instance of [PaymentMethodStore].
|
||||
static PaymentMethodStore get instance {
|
||||
_instance ??= PaymentMethodStore();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
PaymentMethodStore({CustomerSession? customerSession})
|
||||
: _customerSession = customerSession ?? CustomerSession.instance {
|
||||
_customerSession.addListener(_customerSessionListener);
|
||||
}
|
||||
|
||||
void _customerSessionListener() => dispose();
|
||||
|
||||
/// Refreshes data from the API when the first listener is added.
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
super.addListener(listener);
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a payment method and refresh the store if there are any active listeners.
|
||||
Future<Map<String, dynamic>> attachPaymentMethod(String paymentMethodId) async {
|
||||
final paymentMethodFuture = await _customerSession.attachPaymentMethod(paymentMethodId);
|
||||
await refresh();
|
||||
return paymentMethodFuture;
|
||||
}
|
||||
|
||||
/// Detach a payment method and refresh the store if there are any active listeners.
|
||||
Future<Map> detachPaymentMethod(String paymentMethodId) async {
|
||||
final paymentMethodFuture = await _customerSession.detachPaymentMethod(paymentMethodId);
|
||||
await refresh();
|
||||
return paymentMethodFuture;
|
||||
}
|
||||
|
||||
/// Refresh the store if there are any active listeners.
|
||||
Future<void> refresh() async {
|
||||
final paymentMethodFuture = _customerSession.listPaymentMethods(limit: 100);
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
return paymentMethodFuture.then((value) {
|
||||
final List listData = value['data'] ?? <PaymentMethod>[];
|
||||
paymentMethods.clear();
|
||||
if (listData.isNotEmpty) {
|
||||
paymentMethods.addAll(listData
|
||||
.map((item) => PaymentMethod(item['id'], item['card']['last4'], item['card']['brand'],
|
||||
DateTime(item['card']['exp_year'], item['card']['exp_month'])))
|
||||
.toList());
|
||||
}
|
||||
}).whenComplete(() {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the store, notify all active listeners and dispose the ChangeNotifier.
|
||||
@override
|
||||
void dispose() {
|
||||
_customerSession.removeListener(_customerSessionListener);
|
||||
paymentMethods.clear();
|
||||
notifyListeners();
|
||||
if (identical(this, _instance)) {
|
||||
_instance = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
55
lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart
vendored
Normal file
55
lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../stripe_error.dart';
|
||||
|
||||
import 'models.dart';
|
||||
|
||||
typedef CreateSetupIntent = Future<IntentClientSecret> Function();
|
||||
typedef CreatePaymentIntent = Future<IntentClientSecret> Function(int amount);
|
||||
|
||||
class StripeUiOptions {
|
||||
static CreateSetupIntent? createSetupIntent;
|
||||
static CreatePaymentIntent? createPaymentIntent;
|
||||
static String Function(BuildContext context, String currency, int amount) formatCurrency = _defaultFormatCurrency;
|
||||
|
||||
static Widget Function(BuildContext context, Map<String, dynamic> paymentIntent, void Function()? onPressed)
|
||||
payButtonBuilder = _defaultPayButtonBuilder;
|
||||
|
||||
static void Function(BuildContext, Map<String, dynamic>) onPaymentFailed = _defaultOnPaymentFailed;
|
||||
static void Function(BuildContext, Map<String, dynamic>) onPaymentSuccess = _defaultOnPaymentSuccess;
|
||||
static void Function(BuildContext, StripeApiException) onPaymentError = _defaultOnPaymentError;
|
||||
|
||||
static String defaultWebReturnUrl = "/";
|
||||
|
||||
static String defaultMobileReturnUrl = "stripesdk://3ds.stripesdk";
|
||||
static bool showTestPaymentMethods = false;
|
||||
}
|
||||
|
||||
String _defaultFormatCurrency(BuildContext context, String currency, int amount) {
|
||||
return "${currency.toUpperCase()}${(amount / 100).toStringAsFixed(2)}";
|
||||
}
|
||||
|
||||
Widget _defaultPayButtonBuilder(BuildContext context, Map<String, dynamic> paymentIntent, void Function()? onPressed) {
|
||||
return ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text('Pay ${StripeUiOptions.formatCurrency(context, paymentIntent['currency'], paymentIntent['amount'])}'),
|
||||
);
|
||||
}
|
||||
|
||||
void _defaultOnPaymentSuccess(BuildContext context, Map<String, dynamic> paymentIntent) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..clearSnackBars()
|
||||
..showSnackBar(const SnackBar(content: Text("Payment successfully completed")));
|
||||
}
|
||||
|
||||
void _defaultOnPaymentFailed(BuildContext context, Map<String, dynamic> paymentIntent) {
|
||||
final lastIntent = paymentIntent['last_payment_error'];
|
||||
ScaffoldMessenger.of(context)
|
||||
..clearSnackBars()
|
||||
..showSnackBar(SnackBar(content: Text(lastIntent != null ? lastIntent('message') : "Payment aborted")));
|
||||
}
|
||||
|
||||
void _defaultOnPaymentError(BuildContext context, StripeApiException e) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..clearSnackBars()
|
||||
..showSnackBar(SnackBar(content: Text(e.message)));
|
||||
}
|
||||
26
lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart
vendored
Normal file
26
lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class StripeWebView extends StatelessWidget {
|
||||
const StripeWebView({Key? key, required this.uri, required this.returnUri}) : super(key: key);
|
||||
|
||||
final String uri;
|
||||
final Uri returnUri;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebView(
|
||||
initialUrl: uri,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
navigationDelegate: (navigation) {
|
||||
final uri = Uri.parse(navigation.url);
|
||||
if (uri.scheme == returnUri.scheme &&
|
||||
uri.host == returnUri.host &&
|
||||
uri.queryParameters['requestId'] == returnUri.queryParameters['requestId']) {
|
||||
Navigator.pop(context, true);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
});
|
||||
}
|
||||
}
|
||||
50
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart
vendored
Normal file
50
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
|
||||
/// Form field to edit a credit card CVC code, with validation
|
||||
class CardCvcFormField extends StatefulWidget {
|
||||
const CardCvcFormField(
|
||||
{Key? key,
|
||||
this.initialValue,
|
||||
required this.onSaved,
|
||||
required this.validator,
|
||||
required this.onChanged,
|
||||
this.decoration = defaultDecoration,
|
||||
this.textStyle = defaultTextStyle})
|
||||
: super(key: key);
|
||||
|
||||
final String? initialValue;
|
||||
final InputDecoration decoration;
|
||||
final TextStyle textStyle;
|
||||
final void Function(String?) onSaved;
|
||||
final void Function(String) onChanged;
|
||||
final String? Function(String?) validator;
|
||||
|
||||
static const defaultErrorText = 'Invalid CVV';
|
||||
static const defaultDecoration = InputDecoration(border: OutlineInputBorder(), labelText: 'CVV', hintText: 'XXX');
|
||||
static const defaultTextStyle = TextStyle(color: Colors.black);
|
||||
|
||||
@override
|
||||
_CardCvcFormFieldState createState() => _CardCvcFormFieldState();
|
||||
}
|
||||
|
||||
class _CardCvcFormFieldState extends State<CardCvcFormField> {
|
||||
final maskFormatter = MaskTextInputFormatter(mask: '####');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
initialValue: widget.initialValue,
|
||||
autofillHints: const [AutofillHints.creditCardSecurityCode],
|
||||
inputFormatters: [maskFormatter],
|
||||
onChanged: widget.onChanged,
|
||||
validator: widget.validator,
|
||||
onSaved: widget.onSaved,
|
||||
style: widget.textStyle,
|
||||
decoration: widget.decoration,
|
||||
textInputAction: TextInputAction.next,
|
||||
// Use TextInputType.datetime instead of TextInputType.number to fix the numeric keyboard issue on
|
||||
// iOS devices running iOS12 and lower. See: https://github.com/flutter/flutter/issues/58510
|
||||
keyboardType: TextInputType.datetime);
|
||||
}
|
||||
}
|
||||
83
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart
vendored
Normal file
83
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
|
||||
/// Form field to edit a credit card expiration date, with validation.
|
||||
class CardExpiryFormField extends StatefulWidget {
|
||||
const CardExpiryFormField(
|
||||
{Key? key,
|
||||
this.initialMonth,
|
||||
this.initialYear,
|
||||
required this.onSaved,
|
||||
required this.validator,
|
||||
required this.onChanged,
|
||||
this.decoration = defaultDecoration,
|
||||
this.textStyle = defaultTextStyle})
|
||||
: super(key: key);
|
||||
|
||||
final int? initialMonth;
|
||||
final int? initialYear;
|
||||
final InputDecoration decoration;
|
||||
final TextStyle textStyle;
|
||||
final void Function(int?, int?) onSaved;
|
||||
final void Function(int?, int?) onChanged;
|
||||
final String? Function(String?) validator;
|
||||
|
||||
static const defaultLabelText = 'Expiration Date';
|
||||
static const defaultHintText = 'MM/YY';
|
||||
static const defaultErrorText = 'Invalid expiration date';
|
||||
static const defaultMonthMask = '##';
|
||||
static const defaultYearMask = '##';
|
||||
|
||||
static const defaultDecoration =
|
||||
InputDecoration(border: OutlineInputBorder(), labelText: defaultLabelText, hintText: defaultHintText);
|
||||
static const defaultTextStyle = TextStyle(color: Colors.black);
|
||||
|
||||
@override
|
||||
_CardExpiryFormFieldState createState() => _CardExpiryFormFieldState();
|
||||
}
|
||||
|
||||
class _CardExpiryFormFieldState extends State<CardExpiryFormField> {
|
||||
final maskFormatter =
|
||||
MaskTextInputFormatter(mask: '${CardExpiryFormField.defaultMonthMask}/${CardExpiryFormField.defaultYearMask}');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final month = widget.initialMonth?.toString().padLeft(CardExpiryFormField.defaultMonthMask.length, '0');
|
||||
final year = widget.initialYear?.toString().substring(widget.initialYear.toString().length -
|
||||
min(CardExpiryFormField.defaultYearMask.length, widget.initialYear.toString().length));
|
||||
final initial = (month ?? '') + (year ?? '');
|
||||
|
||||
final initialMaskFormatter =
|
||||
MaskTextInputFormatter(mask: '${CardExpiryFormField.defaultMonthMask}/${CardExpiryFormField.defaultYearMask}');
|
||||
|
||||
return TextFormField(
|
||||
validator: widget.validator,
|
||||
initialValue:
|
||||
initialMaskFormatter.formatEditUpdate(const TextEditingValue(), TextEditingValue(text: initial)).text,
|
||||
autofillHints: const [AutofillHints.creditCardExpirationDate],
|
||||
onChanged: (text) {
|
||||
final arr = text.split('/');
|
||||
final month = int.tryParse(arr[0]);
|
||||
int? year;
|
||||
if (arr.length == 2) {
|
||||
year = int.tryParse(arr[1]);
|
||||
}
|
||||
widget.onChanged(month, year);
|
||||
},
|
||||
onSaved: (text) {
|
||||
final arr = text!.split('/');
|
||||
final month = int.tryParse(arr[0]);
|
||||
final year = int.tryParse(arr[1]);
|
||||
widget.onSaved(month, year);
|
||||
},
|
||||
inputFormatters: [maskFormatter],
|
||||
style: widget.textStyle,
|
||||
decoration: widget.decoration,
|
||||
textInputAction: TextInputAction.next,
|
||||
// Use TextInputType.datetime instead of TextInputType.number to fix the numeric keyboard issue on
|
||||
// iOS devices running iOS12 and lower. See: https://github.com/flutter/flutter/issues/58510
|
||||
keyboardType: TextInputType.datetime);
|
||||
}
|
||||
}
|
||||
186
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart
vendored
Normal file
186
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:awesome_card/credit_card.dart';
|
||||
import 'package:awesome_card/style/card_background.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/card.dart';
|
||||
import 'card_cvc_form_field.dart';
|
||||
import 'card_expiry_form_field.dart';
|
||||
import 'card_number_form_field.dart';
|
||||
|
||||
/// Basic form to add or edit a credit card, with complete validation.
|
||||
class CardForm extends StatefulWidget {
|
||||
CardForm(
|
||||
{Key? key,
|
||||
formKey,
|
||||
card,
|
||||
this.cardNumberDecoration,
|
||||
this.cardNumberTextStyle,
|
||||
this.cardExpiryDecoration,
|
||||
this.cardExpiryTextStyle,
|
||||
this.cardCvcDecoration,
|
||||
this.cardCvcTextStyle,
|
||||
this.cardNumberErrorText,
|
||||
this.cardExpiryErrorText,
|
||||
this.cardCvcErrorText,
|
||||
this.cardDecoration,
|
||||
this.postalCodeDecoration,
|
||||
this.postalCodeTextStyle,
|
||||
this.postalCodeErrorText,
|
||||
this.displayAnimatedCard = !kIsWeb && false,
|
||||
this.displayPostalCode = true})
|
||||
: card = card ?? StripeCard(),
|
||||
formKey = formKey ?? GlobalKey(),
|
||||
super(key: key);
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final StripeCard card;
|
||||
final bool displayAnimatedCard;
|
||||
final bool displayPostalCode;
|
||||
final InputDecoration? cardNumberDecoration;
|
||||
final TextStyle? cardNumberTextStyle;
|
||||
final InputDecoration? cardExpiryDecoration;
|
||||
final TextStyle? cardExpiryTextStyle;
|
||||
final InputDecoration? cardCvcDecoration;
|
||||
final TextStyle? cardCvcTextStyle;
|
||||
final InputDecoration? postalCodeDecoration;
|
||||
final TextStyle? postalCodeTextStyle;
|
||||
final String? cardNumberErrorText;
|
||||
final String? postalCodeErrorText;
|
||||
final String? cardExpiryErrorText;
|
||||
final String? cardCvcErrorText;
|
||||
final Decoration? cardDecoration;
|
||||
|
||||
@override
|
||||
_CardFormState createState() => _CardFormState();
|
||||
}
|
||||
|
||||
class _CardFormState extends State<CardForm> {
|
||||
final StripeCard _validationModel = StripeCard();
|
||||
bool cvcHasFocus = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var cardExpiry = 'MM/YY';
|
||||
if (_validationModel.expMonth != null) {
|
||||
cardExpiry = "${_validationModel.expMonth}/${_validationModel.expYear ?? 'YY'}";
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child:
|
||||
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (widget.displayAnimatedCard) _getCreditCardView(cardExpiry),
|
||||
Form(
|
||||
key: widget.formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: CardNumberFormField(
|
||||
initialValue: _validationModel.number ?? widget.card.number,
|
||||
onChanged: (number) {
|
||||
setState(() {
|
||||
_validationModel.number = number;
|
||||
});
|
||||
},
|
||||
validator: (text) => _validationModel.validateNumber()
|
||||
? null
|
||||
: widget.cardNumberErrorText ?? CardNumberFormField.defaultErrorText,
|
||||
textStyle: widget.cardNumberTextStyle ?? CardNumberFormField.defaultTextStyle,
|
||||
onSaved: (text) => widget.card.number = text,
|
||||
decoration: widget.cardNumberDecoration ?? CardNumberFormField.defaultDecoration,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: CardExpiryFormField(
|
||||
initialMonth: _validationModel.expMonth ?? widget.card.expMonth,
|
||||
initialYear: _validationModel.expYear ?? widget.card.expYear,
|
||||
onChanged: (int? month, int? year) {
|
||||
setState(() {
|
||||
_validationModel.expMonth = month;
|
||||
_validationModel.expYear = year;
|
||||
});
|
||||
},
|
||||
onSaved: (int? month, int? year) {
|
||||
widget.card.expMonth = month;
|
||||
widget.card.expYear = year;
|
||||
},
|
||||
validator: (text) => _validationModel.validateDate()
|
||||
? null
|
||||
: widget.cardExpiryErrorText ?? CardExpiryFormField.defaultErrorText,
|
||||
textStyle: widget.cardExpiryTextStyle ?? CardExpiryFormField.defaultTextStyle,
|
||||
decoration: widget.cardExpiryDecoration ?? CardExpiryFormField.defaultDecoration,
|
||||
)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: Focus(
|
||||
onFocusChange: (value) => setState(() => cvcHasFocus = value),
|
||||
child: CardCvcFormField(
|
||||
initialValue: _validationModel.cvc ?? widget.card.cvc,
|
||||
onChanged: (text) => setState(() => _validationModel.cvc = text),
|
||||
onSaved: (text) => widget.card.cvc = text,
|
||||
validator: (text) => _validationModel.validateCVC()
|
||||
? null
|
||||
: widget.cardCvcErrorText ?? CardCvcFormField.defaultErrorText,
|
||||
textStyle: widget.cardCvcTextStyle ?? CardCvcFormField.defaultTextStyle,
|
||||
decoration: widget.cardCvcDecoration ?? CardCvcFormField.defaultDecoration,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.displayPostalCode) _getPostalCodeField(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getPostalCodeField() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: TextFormField(
|
||||
textInputAction: TextInputAction.done,
|
||||
initialValue: _validationModel.postalCode ?? widget.card.postalCode,
|
||||
onChanged: (text) => setState(() => _validationModel.postalCode = text),
|
||||
onSaved: (text) => widget.card.postalCode = text,
|
||||
autofillHints: const [AutofillHints.postalCode],
|
||||
validator: (text) =>
|
||||
_validationModel.isPostalCodeValid() ? null : widget.postalCodeErrorText ?? 'Invalid postal code',
|
||||
style: widget.postalCodeTextStyle ?? const TextStyle(color: Colors.black),
|
||||
decoration: widget.postalCodeDecoration ??
|
||||
const InputDecoration(border: OutlineInputBorder(), labelText: 'Postal code'),
|
||||
// Use TextInputType.datetime instead of TextInputType.number to fix the numeric keyboard issue on
|
||||
// iOS devices running iOS12 and lower. See: https://github.com/flutter/flutter/issues/58510
|
||||
keyboardType: TextInputType.streetAddress),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getCreditCardView(String cardExpiry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: CreditCard(
|
||||
cardNumber: _validationModel.number ?? '',
|
||||
cardExpiry: cardExpiry,
|
||||
cvv: _validationModel.cvc ?? '',
|
||||
frontBackground: widget.cardDecoration != null
|
||||
? Container(
|
||||
width: double.maxFinite,
|
||||
height: double.maxFinite,
|
||||
decoration: widget.cardDecoration,
|
||||
)
|
||||
: CardBackgrounds.black,
|
||||
backBackground: CardBackgrounds.white,
|
||||
showBackSide: cvcHasFocus,
|
||||
showShadow: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart
vendored
Normal file
60
lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
|
||||
/// Form field to edit a credit card number, with validation.
|
||||
class CardNumberFormField extends StatefulWidget {
|
||||
const CardNumberFormField(
|
||||
{Key? key,
|
||||
this.initialValue,
|
||||
required this.onSaved,
|
||||
required this.validator,
|
||||
this.onChanged,
|
||||
this.decoration = defaultDecoration,
|
||||
this.textStyle = defaultTextStyle,
|
||||
this.textEditingController})
|
||||
: super(key: key);
|
||||
|
||||
final void Function(String?) onSaved;
|
||||
final void Function(String)? onChanged;
|
||||
final String? Function(String?) validator;
|
||||
final String? initialValue;
|
||||
final InputDecoration decoration;
|
||||
final TextStyle textStyle;
|
||||
final TextEditingController? textEditingController;
|
||||
|
||||
static const defaultLabelText = 'Card number';
|
||||
static const defaultHintText = 'XXXX XXXX XXXX XXXX';
|
||||
static const defaultErrorText = 'Invalid card number';
|
||||
|
||||
static const defaultDecoration = InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: defaultLabelText,
|
||||
hintText: defaultHintText,
|
||||
);
|
||||
static const defaultTextStyle = TextStyle(color: Colors.black);
|
||||
|
||||
@override
|
||||
_CardNumberFormFieldState createState() => _CardNumberFormFieldState();
|
||||
}
|
||||
|
||||
class _CardNumberFormFieldState extends State<CardNumberFormField> {
|
||||
final maskFormatter = MaskTextInputFormatter(mask: '#### #### #### ####');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
initialValue: widget.initialValue,
|
||||
controller: widget.textEditingController,
|
||||
inputFormatters: [maskFormatter],
|
||||
autofocus: true,
|
||||
autofillHints: const [AutofillHints.creditCardNumber],
|
||||
onSaved: widget.onSaved,
|
||||
validator: widget.validator,
|
||||
onChanged: widget.onChanged,
|
||||
decoration: widget.decoration,
|
||||
style: widget.textStyle,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
);
|
||||
}
|
||||
}
|
||||
174
lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart
vendored
Normal file
174
lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../stripe_sdk_ui.dart';
|
||||
|
||||
typedef OnPaymentMethodSelected = void Function(String?);
|
||||
|
||||
enum SelectorType { radioButton, dropdownButton }
|
||||
|
||||
class PaymentMethodSelector extends StatefulWidget {
|
||||
PaymentMethodSelector({
|
||||
required this.onChanged,
|
||||
PaymentMethodStore? paymentMethodStore,
|
||||
this.initialPaymentMethodId,
|
||||
this.selectorType = SelectorType.radioButton,
|
||||
Key? key,
|
||||
this.selectFirstByDefault = false,
|
||||
}) : _paymentMethodStore = paymentMethodStore ?? PaymentMethodStore.instance,
|
||||
super(key: key);
|
||||
|
||||
final String? initialPaymentMethodId;
|
||||
final OnPaymentMethodSelected onChanged;
|
||||
final PaymentMethodStore _paymentMethodStore;
|
||||
final bool selectFirstByDefault;
|
||||
final SelectorType selectorType;
|
||||
|
||||
@override
|
||||
_PaymentMethodSelectorState createState() => _PaymentMethodSelectorState();
|
||||
}
|
||||
|
||||
class _PaymentMethodSelectorState extends State<PaymentMethodSelector> {
|
||||
List<PaymentMethod>? _paymentMethods;
|
||||
|
||||
PaymentMethod? _selectedPaymentMethod;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget._paymentMethodStore.addListener(_updateState);
|
||||
_updateState();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget._paymentMethodStore.removeListener(_updateState);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_paymentMethods = widget._paymentMethodStore.paymentMethods;
|
||||
_isLoading = widget._paymentMethodStore.isLoading;
|
||||
if (!_paymentMethods!.contains(_selectedPaymentMethod) || _selectedPaymentMethod == null) {
|
||||
if (widget.selectFirstByDefault && _selectedPaymentMethod == null) {
|
||||
_selectedPaymentMethod = _paymentMethods?.firstOrNull;
|
||||
} else {
|
||||
_selectedPaymentMethod = null;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onChanged(_selectedPaymentMethod?.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (!_isLoading) _buildSelector() else _buildLoadingIndicator(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final id = await Navigator.push(
|
||||
context, AddPaymentMethodScreen.route(paymentMethodStore: widget._paymentMethodStore));
|
||||
if (id != null) {
|
||||
await widget._paymentMethodStore.refresh();
|
||||
setState(() {
|
||||
_selectedPaymentMethod = _getPaymentMethodById(id);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('+ Add card')),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final _ = await Navigator.push(
|
||||
context,
|
||||
PaymentMethodsScreen.route(
|
||||
title: 'Payment methods', paymentMethodStore: widget._paymentMethodStore));
|
||||
},
|
||||
child: const Text('Manage cards')),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelector() {
|
||||
switch (widget.selectorType) {
|
||||
case SelectorType.radioButton:
|
||||
return _buildRadioListSelector();
|
||||
case SelectorType.dropdownButton:
|
||||
return _buildDropdownSelector();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildRadioListSelector() {
|
||||
return ListView.builder(
|
||||
itemCount: _paymentMethods?.length ?? 0,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _paymentMethods![index];
|
||||
return RadioListTile<String?>(
|
||||
title: Text(item.brand.toUpperCase()),
|
||||
secondary: Text(item.last4),
|
||||
subtitle: Text(item.getExpirationAsString()),
|
||||
value: item.id,
|
||||
groupValue: _selectedPaymentMethod?.id,
|
||||
onChanged: (value) => setState(() {
|
||||
_selectedPaymentMethod = _getPaymentMethodById(value);
|
||||
widget.onChanged(_selectedPaymentMethod?.id);
|
||||
}));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownSelector() {
|
||||
if (_paymentMethods?.isEmpty == true) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.onBackground),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: DropdownButton<String>(
|
||||
underline: const SizedBox.shrink(),
|
||||
value: _selectedPaymentMethod?.id,
|
||||
items: _paymentMethods
|
||||
?.map((item) => DropdownMenuItem(
|
||||
value: item.id,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text('${item.brand.toUpperCase()} **** **** **** ${item.last4}'),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = _getPaymentMethodById(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PaymentMethod? _getPaymentMethodById(String? paymentMethodId) {
|
||||
return _paymentMethods?.singleWhereOrNull((item) => item.id == paymentMethodId);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
47
lib/vendor/stripe_sdk/lib/src/util/model_utils.dart
vendored
Normal file
47
lib/vendor/stripe_sdk/lib/src/util/model_utils.dart
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import '../util/stripe_text_utils.dart';
|
||||
|
||||
class ModelUtils {
|
||||
/// Check to see whether the input string is a whole, positive number.
|
||||
///
|
||||
/// @param value the input string to test
|
||||
/// @return {@code true} if the input value consists entirely of integers
|
||||
static bool isWholePositiveNumber(String? value) {
|
||||
return value != null && isDigitsOnly(value);
|
||||
}
|
||||
|
||||
/// Determines whether the input year-month pair has passed.
|
||||
///
|
||||
/// @param year the input year, as a two or four-digit integer
|
||||
/// @param month the input month
|
||||
/// @param now the current time
|
||||
/// @return {@code true} if the input time has passed the specified current time,
|
||||
/// {@code false} otherwise.
|
||||
static bool hasMonthPassed(int year, int month, DateTime now) {
|
||||
if (hasYearPassed(year, now)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Expires at end of specified month
|
||||
return normalizeYear(year, now) == now.year && month < now.month;
|
||||
}
|
||||
|
||||
/// Determines whether or not the input year has already passed.
|
||||
///
|
||||
/// @param year the input year, as a two or four-digit integer
|
||||
/// @param now, the current time
|
||||
/// @return {@code true} if the input year has passed the year of the specified current time
|
||||
/// {@code false} otherwise.
|
||||
static bool hasYearPassed(int year, DateTime now) {
|
||||
final normalized = normalizeYear(year, now);
|
||||
return normalized < now.year;
|
||||
}
|
||||
|
||||
static int normalizeYear(int year, DateTime now) {
|
||||
if (year < 100 && year >= 0) {
|
||||
final currentYear = now.year.toString();
|
||||
final prefix = currentYear.substring(0, currentYear.length - 2);
|
||||
year = int.parse('$prefix$year');
|
||||
}
|
||||
return year;
|
||||
}
|
||||
}
|
||||
67
lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart
vendored
Normal file
67
lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
|
||||
const String nullValue = 'null';
|
||||
|
||||
/// Calls through to {@link JSONObject#optString(String)} while safely
|
||||
/// converting the raw string "null" and the empty string to {@code null}. Will not throw
|
||||
/// an exception if the field isn't found.
|
||||
///
|
||||
/// @param jsonObject the input object
|
||||
/// @param fieldName the optional field name
|
||||
/// @return the value stored in the field, or {@code null} if the field isn't present
|
||||
String? optString(Map<String, dynamic> json, String fieldName) {
|
||||
return nullIfNullOrEmpty(json[fieldName] ?? '');
|
||||
}
|
||||
|
||||
/// Calls through to {@link JSONObject#optInt(String)} only in the case that the
|
||||
/// key exists. This returns {@code null} if the key is not in the object.
|
||||
///
|
||||
/// @param jsonObject the input object
|
||||
/// @param fieldName the required field name
|
||||
/// @return the value stored in the requested field, or {@code null} if the key is not present
|
||||
bool? optBoolean(Map<String, dynamic> json, String fieldName) {
|
||||
return json[fieldName];
|
||||
}
|
||||
|
||||
/// Calls through to {@link JSONObject#optInt(String)} only in the case that the
|
||||
/// key exists. This returns {@code null} if the key is not in the object.
|
||||
///
|
||||
/// @param jsonObject the input object
|
||||
/// @param fieldName the required field name
|
||||
/// @return the value stored in the requested field, or {@code null} if the key is not present
|
||||
int? optInteger(Map<String, dynamic> json, String fieldName) {
|
||||
return json[fieldName];
|
||||
}
|
||||
|
||||
/// Calls through to {@link JSONObject#optString(String)} while safely converting
|
||||
/// the raw string "null" and the empty string to {@code null}, along with any value that isn't
|
||||
/// a two-character string.
|
||||
/// @param jsonObject the object from which to retrieve the country code
|
||||
/// @param fieldName the name of the field in which the country code is stored
|
||||
/// @return a two-letter country code if one is found, or {@code null}
|
||||
String? optCountryCode(Map<String, dynamic> json, String fieldName) {
|
||||
final value = optString(json, fieldName);
|
||||
if (value != null && value.length == 2) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Calls through to {@link JSONObject#optString(String)} while safely converting
|
||||
/// the raw string "null" and the empty string to {@code null}, along with any value that isn't
|
||||
/// a three-character string.
|
||||
/// @param jsonObject the object from which to retrieve the currency code
|
||||
/// @param fieldName the name of the field in which the currency code is stored
|
||||
/// @return a three-letter currency code if one is found, or {@code null}
|
||||
String? optCurrency(Map<String, dynamic> json, String fieldName) {
|
||||
final value = optString(json, fieldName);
|
||||
if (value != null && value.length == 3) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
String? nullIfNullOrEmpty(String possibleNull) {
|
||||
return ((nullValue == possibleNull) || (possibleNull.isEmpty)) ? null : possibleNull;
|
||||
}
|
||||
78
lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart
vendored
Normal file
78
lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/// Util Array for converting bytes to a hex string.
|
||||
/// {@url http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java}
|
||||
|
||||
|
||||
|
||||
const String hexArray = '0123456789ABCDEF';
|
||||
|
||||
///Swap {@code null} for blank text values.
|
||||
///
|
||||
/// @param value an input string that may or may not be entirely whitespace
|
||||
/// @return {@code null} if the string is entirely whitespace, otherwise the input value
|
||||
///
|
||||
String? nullIfBlank(String? value) {
|
||||
if (isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// A checker for whether or not the input value is entirely whitespace. This is slightly more
|
||||
/// aggressive than the android TextUtils#isEmpty method, which only returns true for
|
||||
/// {@code null} or {@code ""}.
|
||||
///
|
||||
/// @param value a possibly blank input string value
|
||||
/// @return {@code true} if and only if the value is all whitespace, {@code null}, or empty
|
||||
bool isBlank(String? value) {
|
||||
return value == null || value.trim().isEmpty;
|
||||
}
|
||||
|
||||
/// Converts a card number that may have spaces between the numbers into one without any spaces.
|
||||
/// Note: method does not check that all characters are digits or spaces.
|
||||
///
|
||||
/// @param cardNumberWithSpaces a card number, for instance "4242 4242 4242 4242"
|
||||
/// @return the input number minus any spaces, for instance "4242424242424242".
|
||||
/// Returns {@code null} if the input was {@code null} or all spaces.
|
||||
String? removeSpacesAndHyphens(String? cardNumberWithSpaces) {
|
||||
if (isBlank(cardNumberWithSpaces)) {
|
||||
return null;
|
||||
}
|
||||
return cardNumberWithSpaces!.replaceAll(RegExp(r'\s+|\-+'), '');
|
||||
}
|
||||
|
||||
/// Check to see if the input number has any of the given prefixes.
|
||||
///
|
||||
/// @param number the number to test
|
||||
/// @param prefixes the prefixes to test against
|
||||
/// @return {@code true} if number begins with any of the input prefixes
|
||||
bool hasAnyPrefix(String? number, List<String> prefixes) {
|
||||
if (number == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var prefix in prefixes) {
|
||||
if (number.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO function isn't used and the same as isDigitsOnly() -> remove?
|
||||
bool isDigit(String? s) {
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
return int.tryParse(s) != null;
|
||||
}
|
||||
|
||||
bool isDigitsOnly(String? s) {
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
return int.tryParse(s) != null;
|
||||
}
|
||||
|
||||
int? getNumericValue(String s) {
|
||||
return int.tryParse(s);
|
||||
}
|
||||
8
lib/vendor/stripe_sdk/lib/stripe_sdk.dart
vendored
Normal file
8
lib/vendor/stripe_sdk/lib/stripe_sdk.dart
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// A library to communicate with the Stripe API from Dart.
|
||||
library stripe_sdk;
|
||||
|
||||
export 'src/customer_session.dart';
|
||||
export 'src/stripe.dart';
|
||||
export 'src/stripe_api.dart';
|
||||
export 'src/stripe_api_handler.dart';
|
||||
export 'src/stripe_error.dart';
|
||||
15
lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart
vendored
Normal file
15
lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/// UI widgets, screens and helpers for Stripe and Stripe SDK.
|
||||
library stripe_sdk_ui;
|
||||
|
||||
export 'src/models/card.dart';
|
||||
export 'src/ui/checkout_page.dart';
|
||||
export 'src/ui/models.dart';
|
||||
export 'src/ui/screens/add_payment_method_screen.dart';
|
||||
export 'src/ui/screens/payment_methods_screen.dart';
|
||||
export 'src/ui/stores/payment_method_store.dart';
|
||||
export 'src/ui/stripe_ui.dart';
|
||||
export 'src/ui/widgets/card_cvc_form_field.dart';
|
||||
export 'src/ui/widgets/card_expiry_form_field.dart';
|
||||
export 'src/ui/widgets/card_form.dart';
|
||||
export 'src/ui/widgets/card_number_form_field.dart';
|
||||
export 'src/ui/widgets/payment_method_selector.dart';
|
||||
426
lib/vendor/stripe_sdk/pubspec.lock
vendored
Normal file
426
lib/vendor/stripe_sdk/pubspec.lock
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
awesome_card:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: awesome_card
|
||||
sha256: "6455e99492b70c089b1b9d5a0da64e07f5bdd9cb51e5daaca5a1df340f2253d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
credit_card_type_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: credit_card_type_detector
|
||||
sha256: e49d8ce0e76969093982ed9c920f2204bd34e41ddbcaf4589d999af6dc00a3e4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
credit_card_validator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: credit_card_validator
|
||||
sha256: "90a38c5764dee7b4323cbed55118e09d0fb7bd72db3db5ccade4ac07bf475f2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: f857285c8dc0b4f2f77b49a1c083ff8c75223a7549de20f3e607df58cf497a43
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: bfef906cbd4e78ef49ae511d9074aebd1d2251482ef601a280973e8b58b51bbf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.5"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
mask_text_input_formatter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mask_text_input_formatter
|
||||
sha256: "19bb7809c3c2559277e95521b3ee421e1409eb2cc85efd2feb191696c92490f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
simple_animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: simple_animations
|
||||
sha256: "79acf025f196001d4680280f67b4d77ca828459b23498606d2fb130a4961046e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
universal_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: universal_html
|
||||
sha256: "5ff50b7c14d201421cf5230ec389a0591c4deb5c817c9d7ccca3b26fe5f31e34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "568176fc8ab5ac1d88ff0db8ff28659d103851670dda55e83b485664c2309299"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.6"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "2ec66333093cf5dd5103f089c3a7842fcd5581023a571839d3d176ff10244582"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.14"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "0d6b0b9dce05986462e8ba08e51f1909ac63e2e59ebde829c46ad66423f03dee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.14"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "0d06961fca3c1ff122057247c598bfb938b3712923d98671d2df5021fe924567"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: fc3fca6327a51638007b179b5102b4b5c3d23875816f37ae394b37b99a558f4c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: a2bc64b513fd1d8fee7646c7a8330afc897b0973c902d004907fa010e50d323e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "4e24aac2a2960fb9a70a07992e1ba69cb99fbcee48fdf17abe280ce867bfcea2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "74df4f2310165977cbab22c05c5f5c98ce402de96069f53d80e72ecbd73ec3e0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "2da815ee8c8618441b2d8dccd276f24a9a3ccf089445ad9e434d72289576125e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "87c56a34f69867f3aaf0b66df5399a4fc18898b1df8c738b835e62b73059d66f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
70
lib/vendor/stripe_sdk/pubspec.yaml
vendored
Normal file
70
lib/vendor/stripe_sdk/pubspec.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: stripe_sdk
|
||||
description: The only native Stripe library for Flutter. Has complete support for SCA/PSD2, payment intents and the newest Stripe features.
|
||||
version: 5.0.0-nullsafety.1
|
||||
homepage: https://github.com/ezet/stripe_sdk
|
||||
|
||||
environment:
|
||||
sdk: '>=2.13.0 <4.0.0'
|
||||
flutter: "^3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
http: ^0.13.5
|
||||
url_launcher: ^6.1.6
|
||||
mask_text_input_formatter: ^2.4.0
|
||||
# flutter_slidable: ^1.2.0
|
||||
simple_animations: ^4.0.1
|
||||
awesome_card: ^1.1.7
|
||||
credit_card_validator: ^2.0.1
|
||||
collection: ^1.16.0
|
||||
webview_flutter: ^3.0.4
|
||||
universal_html: ^2.0.8
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^1.0.4
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
false_secrets:
|
||||
- example/**
|
||||
Reference in New Issue
Block a user