diff --git a/modules/ensemble/lib/action/stripe_action.dart b/modules/ensemble/lib/action/stripe_action.dart new file mode 100644 index 000000000..55c356c2b --- /dev/null +++ b/modules/ensemble/lib/action/stripe_action.dart @@ -0,0 +1,286 @@ +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/extensions.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:ensemble/framework/stub/deferred_link_manager.dart'; +import 'package:ensemble/framework/stub/stripe_manager.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble/util/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'package:get_it/get_it.dart'; + +class StripeInitAction extends EnsembleAction { + StripeInitAction({ + super.initiator, + required this.publishableKey, + this.onSuccess, + this.onError, + }); + + String publishableKey; + EnsembleAction? onSuccess; + EnsembleAction? onError; + + factory StripeInitAction.fromMap({dynamic payload}) { + if (payload is Map) { + String? publishableKey = Utils.optionalString(payload['publishableKey']); + if (publishableKey == null) { + throw LanguageError('publishableKey is required for StripeInit action'); + } + + EnsembleAction? successAction = EnsembleAction.from(payload['onSuccess']); + + return StripeInitAction( + publishableKey: publishableKey, + onSuccess: successAction, + onError: EnsembleAction.from(payload['onError']), + ); + } + throw LanguageError('StripeInit: Missing inputs for init action'); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + try { + await GetIt.I().stripeInit( + publishableKey: publishableKey, + ); + + if (onSuccess != null) { + return ScreenController().executeAction( + context, + onSuccess!, + event: EnsembleEvent(initiator), + ); + } + } catch (e) { + return ScreenController().executeAction( + context, + onError!, + event: EnsembleEvent( + initiator, + error: 'StripeInit: Unable to initialize - Reason: $e', + ), + ); + } + } +} + +class StripeCreatePaymentIntentAction extends EnsembleAction { + StripeCreatePaymentIntentAction({ + super.initiator, + required this.amount, + required this.currency, + required this.url, + required this.onSuccess, + this.onError, + }); + + int amount; + String currency; + String url; + EnsembleAction? onSuccess; + EnsembleAction? onError; + + factory StripeCreatePaymentIntentAction.fromMap({dynamic payload}) { + if (payload is Map) { + int? amount = Utils.optionalInt(payload['amount']); + if (amount == null) { + throw LanguageError( + 'amount is required for createPaymentIntent action'); + } + + String? currency = Utils.optionalString(payload['currency']); + if (currency == null) { + throw LanguageError( + 'currency is required for createPaymentIntent action'); + } + + String? url = Utils.optionalString(payload['url']); + if (url == null) { + throw LanguageError('url is required for createPaymentIntent action'); + } + + EnsembleAction? successAction = EnsembleAction.from(payload['onSuccess']); + if (successAction == null) { + throw LanguageError( + 'onSuccess() is required for createPaymentIntent action'); + } + + return StripeCreatePaymentIntentAction( + amount: amount, + currency: currency, + url: url, + onSuccess: successAction, + onError: EnsembleAction.from(payload['onError']), + ); + } + throw LanguageError('StripeCreatePaymentIntent: Missing inputs for action'); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + try { + await GetIt.I().createPaymentIntent( + amount: amount, + currency: currency, + url: url, + ); + + if (onSuccess != null) { + return ScreenController().executeAction( + context, + onSuccess!, + event: EnsembleEvent(initiator), + ); + } + } catch (e) { + if (onError != null) { + return ScreenController().executeAction( + context, + onError!, + event: EnsembleEvent( + initiator, + error: + 'StripeCreatePaymentIntent: Unable to create payment intent - Reason: $e', + ), + ); + } + } + } +} + +class StripeShowPaymentSheetAction extends EnsembleAction { + StripeShowPaymentSheetAction({ + super.initiator, + required this.clientSecret, + required this.merchantDisplayName, + required this.merchantCountryCode, + required this.onSuccess, + this.onError, + this.applePay = false, + this.googlePay = false, + this.testEnv = false, + this.style = ThemeMode.system, + this.appearance = const PaymentSheetAppearance(), + this.billingDetails, + this.customFlow, + this.allowsDelayedPaymentMethods = false, + }); + + String clientSecret; + String merchantDisplayName; + String merchantCountryCode; + bool applePay; + bool googlePay; + bool testEnv; + ThemeMode style; + PaymentSheetAppearance appearance; + BillingDetails? billingDetails; + bool? customFlow; + bool allowsDelayedPaymentMethods; + EnsembleAction? onSuccess; + EnsembleAction? onError; + + factory StripeShowPaymentSheetAction.fromMap({dynamic payload}) { + if (payload is Map) { + String? clientSecret = Utils.optionalString(payload['clientSecret']); + if (clientSecret == null) { + throw LanguageError( + 'clientSecret is required for showPaymentSheet action'); + } + + String? merchantDisplayName = + Utils.optionalString(payload['merchantDisplayName']); + if (merchantDisplayName == null) { + throw LanguageError( + 'merchantDisplayName is required for showPaymentSheet action'); + } + + String? merchantCountryCode = + Utils.optionalString(payload['merchantCountryCode']); + if (merchantCountryCode == null) { + throw LanguageError( + 'merchantCountryCode is required for showPaymentSheet action'); + } + + EnsembleAction? successAction = EnsembleAction.from(payload['onSuccess']); + if (successAction == null) { + throw LanguageError( + 'onSuccess() is required for showPaymentSheet action'); + } + + return StripeShowPaymentSheetAction( + clientSecret: clientSecret, + merchantDisplayName: merchantDisplayName, + merchantCountryCode: merchantCountryCode, + onSuccess: successAction, + onError: EnsembleAction.from(payload['onError']), + applePay: payload['applePay'] ?? false, + googlePay: payload['googlePay'] ?? false, + testEnv: payload['testEnv'] ?? false, + style: payload['style'] ?? ThemeMode.system, + appearance: payload['appearance'] ?? const PaymentSheetAppearance(), + billingDetails: BillingDetails( + email: payload['email'], + name: payload['name'], + phone: payload['phone'], + address: Address( + city: payload['city'], + country: payload['country'], + line1: payload['line1'], + line2: payload['line2'], + postalCode: payload['postalCode'], + state: payload['state'], + ), + ), + customFlow: payload['customFlow'], + allowsDelayedPaymentMethods: + payload['allowsDelayedPaymentMethods'] ?? false, + ); + } + throw LanguageError('StripeShowPaymentSheet: Missing inputs for action'); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + try { + await GetIt.I().showPaymentSheet( + clientSecret: clientSecret, + merchantCountryCode: merchantCountryCode, + merchantDisplayName: merchantDisplayName, + allowsDelayedPaymentMethods: allowsDelayedPaymentMethods, + appearance: appearance, + applePay: applePay, + billingDetails: billingDetails, + customFlow: customFlow, + googlePay: googlePay, + style: style, + testEnv: testEnv, + ); + + if (onSuccess != null) { + return ScreenController().executeAction( + context, + onSuccess!, + event: EnsembleEvent(initiator), + ); + } + } catch (e) { + if (onError != null) { + return ScreenController().executeAction( + context, + onError!, + event: EnsembleEvent( + initiator, + error: + 'StripeShowPaymentSheet: Unable to show payment sheet - Reason: $e', + ), + ); + } + } + } +} diff --git a/modules/ensemble/lib/framework/action.dart b/modules/ensemble/lib/framework/action.dart index 0d4300ddf..bc1a8605a 100644 --- a/modules/ensemble/lib/framework/action.dart +++ b/modules/ensemble/lib/framework/action.dart @@ -17,6 +17,7 @@ import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/action/notification_actions.dart'; import 'package:ensemble/action/phone_contact_action.dart'; import 'package:ensemble/action/sign_in_out_action.dart'; +import 'package:ensemble/action/stripe_action.dart'; import 'package:ensemble/action/toast_actions.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/data_context.dart'; @@ -1106,7 +1107,10 @@ enum ActionType { seekAudio, logEvent, getNetworkInfo, - deviceSecurity + deviceSecurity, + stripeInit, + createPaymentIntent, + showPaymentSheet, } /// payload representing an Action to do (navigateToScreen, InvokeAPI, ..) @@ -1273,6 +1277,12 @@ abstract class EnsembleAction { return ResumeAudio.from(payload); } else if (actionType == ActionType.seekAudio) { return SeekAudio.from(payload); + } else if (actionType == ActionType.stripeInit) { + return StripeInitAction.fromMap(payload: payload); + } else if (actionType == ActionType.createPaymentIntent) { + return StripeCreatePaymentIntentAction.fromMap(payload: payload); + } else if (actionType == ActionType.showPaymentSheet) { + return StripeShowPaymentSheetAction.fromMap(payload: payload); } else if (actionType == ActionType.deeplinkInit) { return DeepLinkInitAction.fromMap(payload: payload); } else if (actionType == ActionType.authenticateByBiometric) { diff --git a/modules/ensemble/lib/framework/stub/stripe_manager.dart b/modules/ensemble/lib/framework/stub/stripe_manager.dart new file mode 100644 index 000000000..c7cf756ff --- /dev/null +++ b/modules/ensemble/lib/framework/stub/stripe_manager.dart @@ -0,0 +1,67 @@ +import 'package:ensemble/framework/error_handling.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; + +abstract class StripeManager { + Future stripeInit({ + required String publishableKey, + }); + + Future createPaymentIntent({ + required int amount, + required String currency, + required String url, + }); + + Future showPaymentSheet({ + required String clientSecret, + required String merchantDisplayName, + required String merchantCountryCode, + bool applePay = false, + bool googlePay = false, + bool testEnv = false, + ThemeMode style = ThemeMode.system, + PaymentSheetAppearance appearance = const PaymentSheetAppearance(), + BillingDetails? billingDetails, + bool? customFlow, + bool allowsDelayedPaymentMethods = false, + }); +} + +class StripeManagerStub extends StripeManager { + @override + Future stripeInit({ + required String publishableKey, + }) { + throw ConfigError( + "Stripe Manager is not enabled. Please review the Ensemble documentation."); + } + + @override + Future createPaymentIntent({ + required int amount, + required String currency, + required String url, + }) { + throw ConfigError( + "Stripe Manager is not enabled. Please review the Ensemble documentation."); + } + + @override + Future showPaymentSheet({ + required String clientSecret, + required String merchantDisplayName, + required String merchantCountryCode, + bool applePay = false, + bool googlePay = false, + bool testEnv = false, + ThemeMode style = ThemeMode.system, + PaymentSheetAppearance appearance = const PaymentSheetAppearance(), + BillingDetails? billingDetails, + bool? customFlow, + bool allowsDelayedPaymentMethods = false, + }) { + throw ConfigError( + "Stripe Manager is not enabled. Please review the Ensemble documentation."); + } +} diff --git a/modules/ensemble/pubspec.yaml b/modules/ensemble/pubspec.yaml index a9ada54a5..7953931f5 100644 --- a/modules/ensemble/pubspec.yaml +++ b/modules/ensemble/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: flutter_rating_bar: ^4.0.0 fl_chart: ^0.62.0 signature: ^5.2.1 + flutter_stripe: ^11.0.0 ensemble_icons: git: diff --git a/modules/stripe/CHANGELOG.md b/modules/stripe/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/modules/stripe/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/modules/stripe/README.md b/modules/stripe/README.md new file mode 100644 index 000000000..8b55e735b --- /dev/null +++ b/modules/stripe/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/modules/stripe/lib/stripe_manager.dart b/modules/stripe/lib/stripe_manager.dart new file mode 100644 index 000000000..abb16d59c --- /dev/null +++ b/modules/stripe/lib/stripe_manager.dart @@ -0,0 +1,94 @@ +import 'package:ensemble/framework/stub/stripe_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class StripeManagerImpl extends StripeManager { + static final StripeManagerImpl _instance = StripeManagerImpl._internal(); + + StripeManagerImpl._internal(); + + factory StripeManagerImpl() { + return _instance; + } + + @override + Future stripeInit({ + required String publishableKey, + }) async { + try { + print('Hello'); + Stripe.publishableKey = publishableKey; + } catch (e) { + print(e); + } + } + + @override + Future createPaymentIntent({ + required int amount, + required String currency, + required String url, + }) async { + try { + final response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'amount': amount, + 'currency': currency, + }), + ); + + if (response.statusCode == 200) { + final res = jsonDecode(response.body)['client_secret']; + print(res); + } else {} + } catch (e) { + print(e); + } + } + + @override + Future showPaymentSheet({ + required String clientSecret, + required String merchantDisplayName, + required String merchantCountryCode, + bool applePay = false, + bool googlePay = false, + bool testEnv = false, + ThemeMode style = ThemeMode.system, + PaymentSheetAppearance appearance = const PaymentSheetAppearance(), + BillingDetails? billingDetails, + bool? customFlow, + bool allowsDelayedPaymentMethods = false, + }) async { + try { + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: clientSecret, + merchantDisplayName: merchantDisplayName, + style: style, + appearance: appearance, + billingDetails: billingDetails, + allowsDelayedPaymentMethods: allowsDelayedPaymentMethods, + customFlow: customFlow ?? false, + applePay: applePay + ? PaymentSheetApplePay(merchantCountryCode: merchantCountryCode) + : null, + googlePay: googlePay + ? PaymentSheetGooglePay( + merchantCountryCode: merchantCountryCode, testEnv: testEnv) + : null, + ), + ); + + await Stripe.instance.presentPaymentSheet(); + } catch (e) { + print(e); + } + } +} diff --git a/modules/stripe/pubspec.yaml b/modules/stripe/pubspec.yaml new file mode 100644 index 000000000..b4ca31442 --- /dev/null +++ b/modules/stripe/pubspec.yaml @@ -0,0 +1,57 @@ +name: ensemble_stripe +description: "A new Flutter package project." +version: 0.0.1 + +environment: + sdk: ">=3.2.1 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + ensemble: + path: ../ensemble + flutter_stripe: ^11.0.0 + html: ^0.15.4 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# 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 packages. +flutter: + + # 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 diff --git a/modules/stripe/test/stripe_test.dart b/modules/stripe/test/stripe_test.dart new file mode 100644 index 000000000..5b3558f45 --- /dev/null +++ b/modules/stripe/test/stripe_test.dart @@ -0,0 +1,16 @@ +import 'package:stripe/stripe.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final awesome = Awesome(); + + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () { + expect(awesome.isAwesome, isTrue); + }); + }); +} diff --git a/starter/devtools_options.yaml b/starter/devtools_options.yaml new file mode 100644 index 000000000..7e7e7f67d --- /dev/null +++ b/starter/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/starter/lib/generated/ensemble_modules.dart b/starter/lib/generated/ensemble_modules.dart index 97fab7d22..7f1cddddc 100644 --- a/starter/lib/generated/ensemble_modules.dart +++ b/starter/lib/generated/ensemble_modules.dart @@ -10,8 +10,10 @@ import 'package:ensemble/framework/stub/deferred_link_manager.dart'; import 'package:ensemble/framework/stub/file_manager.dart'; import 'package:ensemble/framework/stub/contacts_manager.dart'; import 'package:ensemble/framework/stub/plaid_link_manager.dart'; +import 'package:ensemble/framework/stub/stripe_manager.dart'; import 'package:ensemble/module/auth_module.dart'; import 'package:ensemble/module/location_module.dart'; +import 'package:ensemble_stripe/stripe_manager.dart'; //import 'package:ensemble_network_info/network_info.dart'; //import 'package:ensemble_firebase_analytics/firebase_analytics.dart'; // import 'package:ensemble_location/location_module.dart'; @@ -65,6 +67,7 @@ class EnsembleModules { static const useBracket = false; static const useNetworkInfo = false; + static const useStripe = true; // widgets @@ -164,5 +167,12 @@ class EnsembleModules { GetIt.I.registerSingleton(NetworkInfoManagerStub()); } + + if (useStripe) { + //uncomment to enable network info + GetIt.I.registerSingleton(StripeManagerImpl()); + } else { + GetIt.I.registerSingleton(StripeManagerStub()); + } } } diff --git a/starter/pubspec.yaml b/starter/pubspec.yaml index 665c2d7ce..6fbb9d5c0 100644 --- a/starter/pubspec.yaml +++ b/starter/pubspec.yaml @@ -18,7 +18,7 @@ version: 1.0.0+1 environment: sdk: ">=3.0.6 <4.0.0" - flutter: '3.19.3' + flutter: "3.19.3" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -34,10 +34,7 @@ dependencies: # embed Ensemble as a sibling project ensemble: - git: - url: https://github.com/EnsembleUI/ensemble.git - ref: main - path: modules/ensemble + path: ../modules/ensemble # Uncomment to enable camera module # ensemble_camera: @@ -104,11 +101,15 @@ dependencies: # path: modules/bracket # Uncomment to enable NetworkInfo -# ensemble_network_info: -# git: -# url: https://github.com/EnsembleUI/ensemble.git -# ref: main -# path: modules/ensemble_network_info + # ensemble_network_info: + # git: + # url: https://github.com/EnsembleUI/ensemble.git + # ref: main + # path: modules/ensemble_network_info + + # Uncomment to enable stripe + ensemble_stripe: + path: ../modules/stripe # The following adds the Cupertino Icons font to your application.