From 909fdf46d640827a6b70a7787475384fa6d357cf Mon Sep 17 00:00:00 2001 From: peima Date: Sun, 12 Jul 2026 04:34:44 +0800 Subject: [PATCH] final2 --- lib/utils/mini_stripe_api.dart | 140 ++++++ lib/utils/mini_stripe_api_handler.dart | 164 +++++++ lib/vendor/countdown/countdown.dart | 7 + lib/vendor/countdown/src/countdown_base.dart | 90 ++++ .../double_back_to_close_app.dart | 98 ++++ .../flappy_search_bar/flappy_search_bar.dart | 449 ++++++++++++++++++ lib/vendor/flappy_search_bar/scaled_tile.dart | 31 ++ .../flappy_search_bar/search_bar_style.dart | 12 + .../stripe_sdk/.github/workflows/publish.yaml | 20 + lib/vendor/stripe_sdk/.gitignore | 140 ++++++ lib/vendor/stripe_sdk/.gitignore.old | 74 +++ lib/vendor/stripe_sdk/.metadata | 10 + lib/vendor/stripe_sdk/CHANGELOG.md | 290 +++++++++++ lib/vendor/stripe_sdk/LICENSE | 26 + lib/vendor/stripe_sdk/README.md | 247 ++++++++++ lib/vendor/stripe_sdk/analysis_options.yaml | 16 + .../stripe_sdk/lib/src/customer_session.dart | 148 ++++++ .../lib/src/ephemeral_key_manager.dart | 119 +++++ .../stripe_sdk/lib/src/models/card.dart | 93 ++++ lib/vendor/stripe_sdk/lib/src/stripe.dart | 268 +++++++++++ lib/vendor/stripe_sdk/lib/src/stripe_api.dart | 139 ++++++ .../lib/src/stripe_api_handler.dart | 185 ++++++++ .../stripe_sdk/lib/src/stripe_error.dart | 67 +++ .../stripe_sdk/lib/src/ui/checkout_page.dart | 121 +++++ lib/vendor/stripe_sdk/lib/src/ui/models.dart | 6 + .../stripe_sdk/lib/src/ui/progress_bar.dart | 12 + .../ui/screens/add_payment_method_screen.dart | 144 ++++++ .../ui/screens/payment_methods_screen.dart | 177 +++++++ .../src/ui/stores/payment_method_store.dart | 89 ++++ .../stripe_sdk/lib/src/ui/stripe_ui.dart | 55 +++ .../lib/src/ui/stripe_web_view.dart | 26 + .../src/ui/widgets/card_cvc_form_field.dart | 50 ++ .../ui/widgets/card_expiry_form_field.dart | 83 ++++ .../lib/src/ui/widgets/card_form.dart | 186 ++++++++ .../ui/widgets/card_number_form_field.dart | 60 +++ .../ui/widgets/payment_method_selector.dart | 174 +++++++ .../stripe_sdk/lib/src/util/model_utils.dart | 47 ++ .../lib/src/util/stripe_json_utils.dart | 67 +++ .../lib/src/util/stripe_text_utils.dart | 78 +++ lib/vendor/stripe_sdk/lib/stripe_sdk.dart | 8 + lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart | 15 + lib/vendor/stripe_sdk/pubspec.lock | 426 +++++++++++++++++ lib/vendor/stripe_sdk/pubspec.yaml | 70 +++ 43 files changed, 4727 insertions(+) create mode 100644 lib/utils/mini_stripe_api.dart create mode 100644 lib/utils/mini_stripe_api_handler.dart create mode 100644 lib/vendor/countdown/countdown.dart create mode 100644 lib/vendor/countdown/src/countdown_base.dart create mode 100644 lib/vendor/double_back_to_close_app/double_back_to_close_app.dart create mode 100644 lib/vendor/flappy_search_bar/flappy_search_bar.dart create mode 100644 lib/vendor/flappy_search_bar/scaled_tile.dart create mode 100644 lib/vendor/flappy_search_bar/search_bar_style.dart create mode 100644 lib/vendor/stripe_sdk/.github/workflows/publish.yaml create mode 100644 lib/vendor/stripe_sdk/.gitignore create mode 100644 lib/vendor/stripe_sdk/.gitignore.old create mode 100644 lib/vendor/stripe_sdk/.metadata create mode 100644 lib/vendor/stripe_sdk/CHANGELOG.md create mode 100644 lib/vendor/stripe_sdk/LICENSE create mode 100644 lib/vendor/stripe_sdk/README.md create mode 100644 lib/vendor/stripe_sdk/analysis_options.yaml create mode 100644 lib/vendor/stripe_sdk/lib/src/customer_session.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/models/card.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/stripe.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/stripe_api.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/stripe_error.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/models.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/util/model_utils.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart create mode 100644 lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart create mode 100644 lib/vendor/stripe_sdk/lib/stripe_sdk.dart create mode 100644 lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart create mode 100644 lib/vendor/stripe_sdk/pubspec.lock create mode 100644 lib/vendor/stripe_sdk/pubspec.yaml diff --git a/lib/utils/mini_stripe_api.dart b/lib/utils/mini_stripe_api.dart new file mode 100644 index 0000000..1cbbf51 --- /dev/null +++ b/lib/utils/mini_stripe_api.dart @@ -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> createToken(Map 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> createPaymentMethod(Map 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> createPaymentMethodFromCard(StripeCard card) async { + return createPaymentMethod(card.toPaymentMethod()); + } + + /// Create a new Source object. + /// https://stripe.com/docs/api/sources/create + Future> createSource(Map 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> 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> confirmPaymentIntent(String clientSecret, {Map? 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> 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> confirmSetupIntent(String clientSecret, {Map? 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]; +} \ No newline at end of file diff --git a/lib/utils/mini_stripe_api_handler.dart b/lib/utils/mini_stripe_api_handler.dart new file mode 100644 index 0000000..9eb793b --- /dev/null +++ b/lib/utils/mini_stripe_api_handler.dart @@ -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> request(RequestMethod method, String path, String key, String? apiVersion, + {final Map? 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> _getStripeResponse(RequestMethod method, final String url, final RequestOptions options, + {final Map? 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 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 errBody = resp[FIELD_ERROR]; + final stripeError = StripeApiError(requestId, errBody); + throw StripeApiException(stripeError); + } else { + return resp; + } + } + + /// + /// + /// + static Map _headers({RequestOptions? options}) { + final 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}'; + } + + // debug headers + final propertyMap = {}; + 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 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(); + } +} diff --git a/lib/vendor/countdown/countdown.dart b/lib/vendor/countdown/countdown.dart new file mode 100644 index 0000000..2f297fd --- /dev/null +++ b/lib/vendor/countdown/countdown.dart @@ -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'; \ No newline at end of file diff --git a/lib/vendor/countdown/src/countdown_base.dart b/lib/vendor/countdown/src/countdown_base.dart new file mode 100644 index 0000000..faa9d88 --- /dev/null +++ b/lib/vendor/countdown/src/countdown_base.dart @@ -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? _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( + onListen: _onListen, + onPause: _onPause, + onResume: _onResume, + onCancel: _onCancel); + } + + Stream 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; + } + } + } +} \ No newline at end of file diff --git a/lib/vendor/double_back_to_close_app/double_back_to_close_app.dart b/lib/vendor/double_back_to_close_app/double_back_to_close_app.dart new file mode 100644 index 0000000..9d2923b --- /dev/null +++ b/lib/vendor/double_back_to_close_app/double_back_to_close_app.dart @@ -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 { + /// 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 _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`.', + ); + } + } +} \ No newline at end of file diff --git a/lib/vendor/flappy_search_bar/flappy_search_bar.dart b/lib/vendor/flappy_search_bar/flappy_search_bar.dart new file mode 100644 index 0000000..4989497 --- /dev/null +++ b/lib/vendor/flappy_search_bar/flappy_search_bar.dart @@ -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 on State> { + void onListChanged(List items) {} + + void onLoading() {} + + void onClear() {} + + void onError(Error error) {} +} + +class SearchBarController { + final List _list = []; + final List _filteredList = []; + final List _sortedList = []; + TextEditingController? _searchQueryController; + String? _lastSearchedText; + Future> 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> 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 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> 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.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.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 extends StatefulWidget { + /// Future returning searched items + final Future> Function(String text) onSearch; + + /// List of items showed by default + final List 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(); +} + +class _SearchBarState extends State> + with TickerProviderStateMixin, _ControllerListener { + bool _loading = false; + Widget? _error; + final _searchQueryController = TextEditingController(); + Timer? _debounce; + bool _animate = false; + List _list = []; + SearchBarController? searchBarController; + + @override + void initState() { + super.initState(); + searchBarController = + widget.searchBarController ?? SearchBarController(); + searchBarController?.setListener(this); + searchBarController?.setTextController(_searchQueryController, widget.minimumChars); + } + + @override + void onListChanged(List 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 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: [ + Padding( + padding: widget.searchBarPadding, + child: Container( + height: 80, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + 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(), + ), + ], + ); + } +} diff --git a/lib/vendor/flappy_search_bar/scaled_tile.dart b/lib/vendor/flappy_search_bar/scaled_tile.dart new file mode 100644 index 0000000..36e9439 --- /dev/null +++ b/lib/vendor/flappy_search_bar/scaled_tile.dart @@ -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); +} diff --git a/lib/vendor/flappy_search_bar/search_bar_style.dart b/lib/vendor/flappy_search_bar/search_bar_style.dart new file mode 100644 index 0000000..4e112a4 --- /dev/null +++ b/lib/vendor/flappy_search_bar/search_bar_style.dart @@ -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))}); +} diff --git a/lib/vendor/stripe_sdk/.github/workflows/publish.yaml b/lib/vendor/stripe_sdk/.github/workflows/publish.yaml new file mode 100644 index 0000000..7a77553 --- /dev/null +++ b/lib/vendor/stripe_sdk/.github/workflows/publish.yaml @@ -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 }} diff --git a/lib/vendor/stripe_sdk/.gitignore b/lib/vendor/stripe_sdk/.gitignore new file mode 100644 index 0000000..7b208a5 --- /dev/null +++ b/lib/vendor/stripe_sdk/.gitignore @@ -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 diff --git a/lib/vendor/stripe_sdk/.gitignore.old b/lib/vendor/stripe_sdk/.gitignore.old new file mode 100644 index 0000000..1985397 --- /dev/null +++ b/lib/vendor/stripe_sdk/.gitignore.old @@ -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 diff --git a/lib/vendor/stripe_sdk/.metadata b/lib/vendor/stripe_sdk/.metadata new file mode 100644 index 0000000..32bc14b --- /dev/null +++ b/lib/vendor/stripe_sdk/.metadata @@ -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 diff --git a/lib/vendor/stripe_sdk/CHANGELOG.md b/lib/vendor/stripe_sdk/CHANGELOG.md new file mode 100644 index 0000000..b61f888 --- /dev/null +++ b/lib/vendor/stripe_sdk/CHANGELOG.md @@ -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 diff --git a/lib/vendor/stripe_sdk/LICENSE b/lib/vendor/stripe_sdk/LICENSE new file mode 100644 index 0000000..cf9a268 --- /dev/null +++ b/lib/vendor/stripe_sdk/LICENSE @@ -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. \ No newline at end of file diff --git a/lib/vendor/stripe_sdk/README.md b/lib/vendor/stripe_sdk/README.md new file mode 100644 index 0000000..43d2154 --- /dev/null +++ b/lib/vendor/stripe_sdk/README.md @@ -0,0 +1,247 @@ +[![pub package](https://img.shields.io/pub/v/stripe_sdk.svg)](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 for a complete demo application, +with a working example backend. + +Demo backend: + +## 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 . + + + + +## Overview + +- The return type for each function is `Future>`, 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 + +- + +Aims to provide high-level functionality similar to the official mobile Stripe SDKs. + +### CustomerSession + +_Requires a Stripe ephemeral key._ + +- +- + +Provides functionality similar to CustomerSession in the Stripe Android SDK. + +### StripeApi + +- + +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(); +final card = StripeCard(); + +final form = CardForm(card: card, formKey: formKey); + +onPressed: () { + if (formKey.currentState.validate()) { + formKey.currentState.save(); + } +} + + +``` + + + + + +## 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 + + + + + + + android:launchMode="singleTop" + + + + + + + + + + + + + + +``` + +### 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 + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + 3ds.stripesdk.io + CFBundleURLSchemes + + stripesdk + + + + + + +``` + + +## 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: +Backend: diff --git a/lib/vendor/stripe_sdk/analysis_options.yaml b/lib/vendor/stripe_sdk/analysis_options.yaml new file mode 100644 index 0000000..d9f1a72 --- /dev/null +++ b/lib/vendor/stripe_sdk/analysis_options.yaml @@ -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 diff --git a/lib/vendor/stripe_sdk/lib/src/customer_session.dart b/lib/vendor/stripe_sdk/lib/src/customer_session.dart new file mode 100644 index 0000000..cc53e3b --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/customer_session.dart @@ -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> 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> 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> 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> 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> 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> 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> updateCustomer(Map 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; + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart b/lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart new file mode 100644 index 0000000..0f592ea --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ephemeral_key_manager.dart @@ -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 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 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 retrieveEphemeralKey() async { + if (_shouldRefreshKey()) { + String key; + try { + key = await ephemeralKeyProvider(defaultApiVersion); + } catch (error) { + log(error.toString()); + rethrow; + } + + try { + Map 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; + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/models/card.dart b/lib/vendor/stripe_sdk/lib/src/models/card.dart new file mode 100644 index 0000000..7a7bf0e --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/models/card.dart @@ -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 toPaymentMethod() { + final Map 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 mapToEdit) { + // Remove all null values; they cause validation errors + final List 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); + } + } + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/stripe.dart b/lib/vendor/stripe_sdk/lib/src/stripe.dart new file mode 100644 index 0000000..2eae455 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/stripe.dart @@ -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> authenticateSetupIntent(String clientSecret, + {String? webReturnPath, required BuildContext context}) async { + final Map 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> confirmSetupIntent(String clientSecret, String paymentMethod, + {String? webReturnPath, required BuildContext context}) async { + var returnUrlForSca = getReturnUrlForSca(webReturnUrl: webReturnPath); + final Map 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> confirmPayment(String paymentIntentClientSecret, BuildContext context, + {String? paymentMethodId}) async { + final data = {'return_url': getReturnUrlForSca()}; + if (paymentMethodId != null) data['payment_method'] = paymentMethodId; + final Map 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> confirmPaymentWithGooglePay(BuildContext context, + {required String paymentIntentClientSecret, required Map paymentResult}) async { + final data = {'return_url': getReturnUrlForSca()}; + final String token = paymentResult['paymentMethodData']['tokenizationData']['token'] as String; + final tokenJson = jsonDecode(token) as Map; + final tokenId = tokenJson['id'] as String; + data['payment_method_data'] = { + 'type': 'card', + "card": {"token": tokenId} + }; + final Map 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> confirmPaymentWithApplePay(BuildContext context, + {required String paymentIntentClientSecret, required Map paymentResult}) async { + final tokenPayload = {}; + 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'] = {}; + 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 = {'return_url': getReturnUrlForSca()}; + data['payment_method_data'] = { + 'type': 'card', + "card": {"token": tokenId} + }; + final Map paymentIntent = await api.confirmPaymentIntent( + paymentIntentClientSecret, + data: data, + ); + return _handlePaymentIntent(paymentIntent, context); + } + + Map _getPaymentMethod(dynamic paymentMethod) { + if (paymentMethod is String) { + return jsonDecode(paymentMethod) as Map; + } else { + return paymentMethod as Map; + } + } + + /// Authenticate a payment. + /// Returns the PaymentIntent. + /// https://stripe.com/docs/payments/payment-intents/android-manual + Future> authenticatePayment(String paymentIntentClientSecret, BuildContext context) async { + final Map 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> _handlePaymentIntent(Map 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> _handleSetupIntent(Map setupIntent, BuildContext context) async { + return _authenticateIntent(setupIntent, context, api.retrieveSetupIntent); + } + + Future> _authenticateIntent(Map intent, BuildContext context, + Future> 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> _authenticateWithBrowser(BuildContext context, String url, Uri returnUri, + Future> Function(String clientSecret) getIntentFunction, String clientSecret) async { + final completer = Completer>(); + + late StreamSubscription 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 _authenticateWithWebView(BuildContext context, String url, Uri returnUri) async { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StripeWebView( + uri: url, + returnUri: returnUri, + ), + )); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/stripe_api.dart b/lib/vendor/stripe_sdk/lib/src/stripe_api.dart new file mode 100644 index 0000000..d3aafb0 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/stripe_api.dart @@ -0,0 +1,139 @@ +import 'dart:async'; + +import 'models/card.dart'; +import 'stripe_api_handler.dart'; + +typedef IntentProvider = Future> 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> createToken(Map 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> createPaymentMethod(Map 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> createPaymentMethodFromCard(StripeCard card) async { + return createPaymentMethod(card.toPaymentMethod()); + } + + /// Create a new Source object. + /// https://stripe.com/docs/api/sources/create + Future> createSource(Map 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> 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> confirmPaymentIntent(String clientSecret, {Map? 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> 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> confirmSetupIntent(String clientSecret, {Map? 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]; +} diff --git a/lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart b/lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart new file mode 100644 index 0000000..8e66644 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/stripe_api_handler.dart @@ -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> request(RequestMethod method, String path, String key, String? apiVersion, + {final Map? params}) { + final options = RequestOptions(key: key, apiVersion: apiVersion, stripeAccount: stripeAccount); + return _getStripeResponse(method, liveApiPath + path, options, params: params); + } + + Future> _getStripeResponse(RequestMethod method, final String url, final RequestOptions options, + {final Map? params}) async { + final Map 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 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 errBody = resp[fieldError]; + final stripeError = StripeApiError(requestId, errBody); + throw StripeApiException(stripeError); + } else { + return resp; + } + } + + /// + /// + /// + static Map _headers({RequestOptions? options}) { + final Map 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 = {}; + 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 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, + }); +} diff --git a/lib/vendor/stripe_sdk/lib/src/stripe_error.dart b/lib/vendor/stripe_sdk/lib/src/stripe_error.dart new file mode 100644 index 0000000..8ae235e --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/stripe_error.dart @@ -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 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; + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart b/lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart new file mode 100644 index 0000000..3a0798f --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/checkout_page.dart @@ -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 Function() createPaymentIntent; + final void Function(BuildContext context, Map paymentIntent) onPaymentSuccess; + final void Function(BuildContext context, Map paymentIntent) onPaymentFailed; + final void Function(BuildContext context, StripeApiException e) onPaymentError; + + CheckoutPage({ + Key? key, + required this.createPaymentIntent, + void Function(BuildContext context, Map paymentIntent)? onPaymentSuccess, + void Function(BuildContext context, Map 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 { + final PaymentMethodStore paymentMethodStore = PaymentMethodStore.instance; + + String? _selectedPaymentMethod; + late Future _clientSecretFuture; + late Future> 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: [ + Center( + child: PaymentMethodSelector( + paymentMethodStore: paymentMethodStore, + initialPaymentMethodId: null, + onChanged: (value) => setState(() { + _selectedPaymentMethod = value; + debugPrint("Selected paymentMethod changed: $value"); + })), + ), + FutureBuilder>( + 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 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; + } + } + }; + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/models.dart b/lib/vendor/stripe_sdk/lib/src/ui/models.dart new file mode 100644 index 0000000..70d8669 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/models.dart @@ -0,0 +1,6 @@ +class IntentClientSecret { + final String clientSecret; + final String? status; + + IntentClientSecret(this.status, this.clientSecret); +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart b/lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart new file mode 100644 index 0000000..12ad04c --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/progress_bar.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +void hideProgressDialog(BuildContext context) { + Navigator.of(context, rootNavigator: true).pop(); +} + +Future showProgressDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center(child: CircularProgressIndicator())); +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart b/lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart new file mode 100644 index 0000000..bc9a3b3 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/screens/add_payment_method_screen.dart @@ -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 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 { + Future? 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: [ + 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 _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 _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']); + } + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart b/lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart new file mode 100644 index 0000000..4ecc647 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/screens/payment_methods_screen.dart @@ -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 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: [ + 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 { + List 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 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 _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: [ + 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'), + ), + ], + ); + }); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart b/lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart new file mode 100644 index 0000000..79dad16 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/stores/payment_method_store.dart @@ -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 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> 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 detachPaymentMethod(String paymentMethodId) async { + final paymentMethodFuture = await _customerSession.detachPaymentMethod(paymentMethodId); + await refresh(); + return paymentMethodFuture; + } + + /// Refresh the store if there are any active listeners. + Future refresh() async { + final paymentMethodFuture = _customerSession.listPaymentMethods(limit: 100); + isLoading = true; + notifyListeners(); + return paymentMethodFuture.then((value) { + final List listData = value['data'] ?? []; + 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(); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart b/lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart new file mode 100644 index 0000000..004210c --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/stripe_ui.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import '../stripe_error.dart'; + +import 'models.dart'; + +typedef CreateSetupIntent = Future Function(); +typedef CreatePaymentIntent = Future 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 paymentIntent, void Function()? onPressed) + payButtonBuilder = _defaultPayButtonBuilder; + + static void Function(BuildContext, Map) onPaymentFailed = _defaultOnPaymentFailed; + static void Function(BuildContext, Map) 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 paymentIntent, void Function()? onPressed) { + return ElevatedButton( + onPressed: onPressed, + child: Text('Pay ${StripeUiOptions.formatCurrency(context, paymentIntent['currency'], paymentIntent['amount'])}'), + ); +} + +void _defaultOnPaymentSuccess(BuildContext context, Map paymentIntent) { + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar(const SnackBar(content: Text("Payment successfully completed"))); +} + +void _defaultOnPaymentFailed(BuildContext context, Map 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))); +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart b/lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart new file mode 100644 index 0000000..f34ae57 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/stripe_web_view.dart @@ -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; + }); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart new file mode 100644 index 0000000..3c0452a --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_cvc_form_field.dart @@ -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 { + 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); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart new file mode 100644 index 0000000..6eda626 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_expiry_form_field.dart @@ -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 { + 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); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart new file mode 100644 index 0000000..a895eaa --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_form.dart @@ -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 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 { + 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: [ + 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, + ), + ); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart new file mode 100644 index 0000000..1dd2562 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/widgets/card_number_form_field.dart @@ -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 { + 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, + ); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart b/lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart new file mode 100644 index 0000000..2a4aba3 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/ui/widgets/payment_method_selector.dart @@ -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 { + List? _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( + 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( + 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(); + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/util/model_utils.dart b/lib/vendor/stripe_sdk/lib/src/util/model_utils.dart new file mode 100644 index 0000000..ac15e1e --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/util/model_utils.dart @@ -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; + } +} diff --git a/lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart b/lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart new file mode 100644 index 0000000..c4028cc --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/util/stripe_json_utils.dart @@ -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 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 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 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 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 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; +} diff --git a/lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart b/lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart new file mode 100644 index 0000000..de45ab7 --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/src/util/stripe_text_utils.dart @@ -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 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); +} diff --git a/lib/vendor/stripe_sdk/lib/stripe_sdk.dart b/lib/vendor/stripe_sdk/lib/stripe_sdk.dart new file mode 100644 index 0000000..687251a --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/stripe_sdk.dart @@ -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'; diff --git a/lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart b/lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart new file mode 100644 index 0000000..a61d30c --- /dev/null +++ b/lib/vendor/stripe_sdk/lib/stripe_sdk_ui.dart @@ -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'; diff --git a/lib/vendor/stripe_sdk/pubspec.lock b/lib/vendor/stripe_sdk/pubspec.lock new file mode 100644 index 0000000..e8b1f2a --- /dev/null +++ b/lib/vendor/stripe_sdk/pubspec.lock @@ -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" diff --git a/lib/vendor/stripe_sdk/pubspec.yaml b/lib/vendor/stripe_sdk/pubspec.yaml new file mode 100644 index 0000000..c1e1cf0 --- /dev/null +++ b/lib/vendor/stripe_sdk/pubspec.yaml @@ -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/**