diff --git a/example/lib/screens/card_payments/web_card_field_customization.dart b/example/lib/screens/card_payments/web_card_field_customization.dart new file mode 100644 index 000000000..59a834b2b --- /dev/null +++ b/example/lib/screens/card_payments/web_card_field_customization.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; + +class WebCardFieldCustomizationExample extends StatefulWidget { + const WebCardFieldCustomizationExample({super.key}); + + @override + State createState() => _WebCardFieldCustomizationExampleState(); +} + +class _WebCardFieldCustomizationExampleState extends State { + late String _fontFamily = 'Montserrat'; + late String _cssSrc = 'https://fonts.googleapis.com/css?family=Montserrat'; + + final _cardEditController = CardEditController(); + final _cardFocusNode = FocusNode(); + + late final _fontFamilyController = TextEditingController(text: _fontFamily); + late final _cssSrcController = TextEditingController(text: _cssSrc); + + @override + void initState() { + super.initState(); + _cardFocusNode.addListener(_onCardFocusChange); + } + + @override + void dispose() { + _cardFocusNode.removeListener(_onCardFocusChange); + _cardFocusNode.dispose(); + _fontFamilyController.dispose(); + _cssSrcController.dispose(); + super.dispose(); + } + + void _onCardFocusChange() { + print('Card focus: ${_cardFocusNode.hasFocus}'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Column( + children: [ + Container( + height: 150, + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), + child: CardField( + controller: _cardEditController, + focusNode: _cardFocusNode, + autofocus: true, + enablePostalCode: true, + style: TextStyle( + fontSize: 20.0, + fontFamily: _fontFamily, + color: Color(0xFF272B2B), + ), + placeholderColor: Colors.blueGrey, + onCardChanged: (_) {}, + webFonts: [ + WebFont( + family: _fontFamily, + cssSrc: _cssSrc, + ), + ], + ), + ), + Divider(height: 1), + Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + TextField( + controller: _fontFamilyController, + decoration: InputDecoration( + labelText: 'Font Family', + ), + onChanged: (value) => setState(() => _fontFamily = value), + ), + TextField( + controller: _cssSrcController, + decoration: InputDecoration( + labelText: 'CSS Source', + ), + onChanged: (value) => setState(() => _cssSrc = value), + ), + SizedBox(height: 16), + Row( + children: [ + ElevatedButton( + onPressed: () { + _cardEditController.focus(); + }, + child: Text('Request Focus'), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + FocusScope.of(context).unfocus(); + }, + child: Text('Lose Focus'), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/screens/screens.dart b/example/lib/screens/screens.dart index d6d26170e..bd437dcc3 100644 --- a/example/lib/screens/screens.dart +++ b/example/lib/screens/screens.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stripe_example/screens/card_payments/web_card_field_customization.dart'; import 'package:stripe_example/screens/checkout/checkout_screen.dart'; import 'package:stripe_example/screens/customer_sheet/customer_sheet_screen.dart'; import 'package:stripe_example/screens/payment_sheet/payment_element/payment_element.dart'; @@ -54,8 +55,7 @@ class ExampleSection extends StatelessWidget { initiallyExpanded: expanded, childrenPadding: EdgeInsets.only(left: 20), title: Text(title), - children: - ListTile.divideTiles(tiles: children, context: context).toList(), + children: ListTile.divideTiles(tiles: children, context: context).toList(), ); } } @@ -121,7 +121,6 @@ class Example extends StatelessWidget { ], ), ], - expanded: true, ), ExampleSection(title: 'Customer sheet', children: [ Example( @@ -133,9 +132,12 @@ class Example extends StatelessWidget { ExampleSection( title: 'Card Payments', children: [ + Example( + title: 'Web Card Field Customization', + builder: (c) => WebCardFieldCustomizationExample(), + ), Example( title: 'Simple - Using webhooks', - style: TextStyle(fontWeight: FontWeight.w600), builder: (c) => WebhookPaymentScreen(), ), Example( @@ -157,6 +159,7 @@ class Example extends StatelessWidget { platformsSupported: [DevicePlatform.android, DevicePlatform.ios], ), ], + expanded: true, ), ExampleSection( title: 'Wallets', @@ -356,11 +359,7 @@ class Example extends StatelessWidget { Example( title: 'Checkout', builder: (c) => CheckoutScreenExample(), - platformsSupported: [ - DevicePlatform.android, - DevicePlatform.ios, - DevicePlatform.web - ], + platformsSupported: [DevicePlatform.android, DevicePlatform.ios, DevicePlatform.web], ), ]; } diff --git a/example/lib/screens/themes.dart b/example/lib/screens/themes.dart index d13b3fdfb..f8c94d37a 100644 --- a/example/lib/screens/themes.dart +++ b/example/lib/screens/themes.dart @@ -65,7 +65,7 @@ class _ThemeCardExampleState extends State { body: Column( children: [ Theme( - data: ThemeData(), + data: theme, child: Container( height: 150, alignment: Alignment.center, @@ -74,20 +74,19 @@ class _ThemeCardExampleState extends State { child: CardField( autofocus: true, enablePostalCode: postalCodeEnabled, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w400, - fontFamily: 'Brandon Text', - color: Color(0xFF272B2B), - ), - placeholderColor: Color(0xFF636D6D), + style: TextStyle(fontFamily: 'Montserrat'), onCardChanged: (_) {}, decoration: InputDecoration( - labelText: theme.inputDecorationTheme.floatingLabelBehavior == - FloatingLabelBehavior.always + labelText: theme.inputDecorationTheme.floatingLabelBehavior == FloatingLabelBehavior.always ? 'Card Field' : null, ), + webFonts: [ + WebFont( + family: 'Montserrat', + cssSrc: 'https://fonts.googleapis.com/css?family=Montserrat', + ), + ], ), ), ), diff --git a/packages/stripe/lib/src/widgets/card_field.dart b/packages/stripe/lib/src/widgets/card_field.dart index de49fa77f..b15c480db 100644 --- a/packages/stripe/lib/src/widgets/card_field.dart +++ b/packages/stripe/lib/src/widgets/card_field.dart @@ -32,6 +32,8 @@ class CardField extends StatefulWidget { this.postalCodeHintText, this.controller, this.androidPlatformViewRenderType = AndroidPlatformViewRenderType.expensiveAndroidView, + this.webFonts, + this.focusNode, }) : super(key: key); /// Decoration related to the input fields. @@ -113,13 +115,19 @@ class CardField extends StatefulWidget { /// Defaults to [AndroidPlatformViewRenderType.expensiveAndroidView] final AndroidPlatformViewRenderType androidPlatformViewRenderType; + final List? webFonts; + + final FocusNode? focusNode; + @override // ignore: library_private_types_in_public_api _CardFieldState createState() => _CardFieldState(); } class _CardFieldState extends State { - final FocusNode _node = FocusNode(debugLabel: 'CardField', descendantsAreFocusable: false); + late final _node = (widget.focusNode ?? FocusNode()) + ..debugLabel = 'CardField' + ..descendantsAreFocusable = false; CardEditController? _fallbackContoller; CardEditController get controller { @@ -184,6 +192,7 @@ class _CardFieldState extends State { onFocus: widget.onFocus, placeholder: placeholder, style: style, + webFonts: widget.webFonts, ) : CustomSingleChildLayout( delegate: const _NegativeMarginLayout(margin: platformMargin), diff --git a/packages/stripe_ios/pubspec_overrides.yaml b/packages/stripe_ios/pubspec_overrides.yaml new file mode 100644 index 000000000..6ccd3c47e --- /dev/null +++ b/packages/stripe_ios/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: stripe_android,stripe_ios,stripe_platform_interface +dependency_overrides: + stripe_platform_interface: + path: ../stripe_platform_interface \ No newline at end of file diff --git a/packages/stripe_platform_interface/lib/src/card_edit_controller.dart b/packages/stripe_platform_interface/lib/src/card_edit_controller.dart index 8423084d2..443a5d749 100644 --- a/packages/stripe_platform_interface/lib/src/card_edit_controller.dart +++ b/packages/stripe_platform_interface/lib/src/card_edit_controller.dart @@ -1,8 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:stripe_platform_interface/src/models/card_field_input.dart'; -const String kDebugPCIMessage = - 'Handling card data manually will break PCI compliance provided by Stripe. ' +const String kDebugPCIMessage = 'Handling card data manually will break PCI compliance provided by Stripe. ' 'Please make sure you understand the severe consecuences of it. ' 'https://stripe.com/docs/security/guide#validating-pci-compliance. \n' 'To handle PCI compliance yourself and allow to edit card data programatically,' @@ -28,8 +27,7 @@ abstract class CardFieldContext { } } - void updateCardDetails( - CardFieldInputDetails value, CardEditController controller) { + void updateCardDetails(CardFieldInputDetails value, CardEditController controller) { controller._updateDetails(value); } } @@ -37,8 +35,7 @@ abstract class CardFieldContext { class CardEditController extends ChangeNotifier { CardEditController({CardFieldInputDetails? initialDetails}) : initalDetails = initialDetails, - _details = - initialDetails ?? const CardFieldInputDetails(complete: false); + _details = initialDetails ?? const CardFieldInputDetails(complete: false); final CardFieldInputDetails? initalDetails; CardFieldInputDetails _details; @@ -75,8 +72,7 @@ class CardEditController extends ChangeNotifier { CardFieldContext? _context; CardFieldContext get context { - assert( - _context != null, 'CardEditController is not attached to any CardView'); + assert(_context != null, 'CardEditController is not attached to any CardView'); return _context!; } } diff --git a/packages/stripe_platform_interface/lib/src/models/web_font.dart b/packages/stripe_platform_interface/lib/src/models/web_font.dart new file mode 100644 index 000000000..796be3dbe --- /dev/null +++ b/packages/stripe_platform_interface/lib/src/models/web_font.dart @@ -0,0 +1,21 @@ +import 'dart:core'; + +class WebFont { + final String family; + final String? src; + final String? display; + final String? style; + final String? unicodeRange; + final String? weight; + final String? cssSrc; + + WebFont({ + required this.family, + this.src, + this.display, + this.style, + this.unicodeRange, + this.weight, + this.cssSrc, + }); +} diff --git a/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart b/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart index 003e8fc91..698c6e266 100644 --- a/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart +++ b/packages/stripe_platform_interface/lib/src/stripe_platform_interface.dart @@ -174,6 +174,7 @@ abstract class StripePlatform extends PlatformInterface { FocusNode? focusNode, bool autofocus = false, bool dangerouslyUpdateFullCardDetails = false, + List? webFonts, }) { throw UnimplementedError(); } diff --git a/packages/stripe_platform_interface/lib/stripe_platform_interface.dart b/packages/stripe_platform_interface/lib/stripe_platform_interface.dart index f0c4588f5..6e5888cde 100644 --- a/packages/stripe_platform_interface/lib/stripe_platform_interface.dart +++ b/packages/stripe_platform_interface/lib/stripe_platform_interface.dart @@ -25,3 +25,4 @@ export 'src/models/setup_intent.dart'; export 'src/models/three_d_secure.dart'; export 'src/models/wallet.dart'; export 'src/stripe_platform_interface.dart'; +export 'src/models/web_font.dart'; diff --git a/packages/stripe_web/lib/src/web_stripe.dart b/packages/stripe_web/lib/src/web_stripe.dart index dfc313cd7..696417392 100644 --- a/packages/stripe_web/lib/src/web_stripe.dart +++ b/packages/stripe_web/lib/src/web_stripe.dart @@ -400,6 +400,7 @@ class WebStripe extends StripePlatform { FocusNode? focusNode, bool autofocus = false, bool dangerouslyUpdateFullCardDetails = false, + List? webFonts, }) { return WebCardField( controller: controller, @@ -414,6 +415,7 @@ class WebStripe extends StripePlatform { focusNode: focusNode, autofocus: autofocus, dangerouslyUpdateFullCardDetails: dangerouslyUpdateFullCardDetails, + webFonts: webFonts, ); } diff --git a/packages/stripe_web/lib/src/widgets/card_field.dart b/packages/stripe_web/lib/src/widgets/card_field.dart index b8d8afc80..9fd0bc7ac 100644 --- a/packages/stripe_web/lib/src/widgets/card_field.dart +++ b/packages/stripe_web/lib/src/widgets/card_field.dart @@ -29,6 +29,7 @@ class WebCardField extends StatefulWidget { this.focusNode, this.autofocus = false, this.dangerouslyUpdateFullCardDetails = false, + this.webFonts, }) : assert(constraints == null || constraints.debugAssertIsValid()), constraints = (width != null || height != null) ? constraints?.tighten(width: width, height: height) ?? @@ -46,6 +47,7 @@ class WebCardField extends StatefulWidget { final bool autofocus; final CardEditController controller; final bool dangerouslyUpdateFullCardDetails; + final List? webFonts; @override WebStripeCardState createState() => WebStripeCardState(); } @@ -87,6 +89,11 @@ class WebStripeCardState extends State with CardFieldContext { ..onBlur(requestBlur) ..onFocus(requestFocus) ..onChange(onCardChanged); + if (widget.autofocus) { + element?.onReady((_) { + element?.focus(); + }); + } }); } }); @@ -124,6 +131,7 @@ class WebStripeCardState extends State with CardFieldContext { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Focus( + autofocus: widget.autofocus, focusNode: _effectiveNode, child: ConstrainedBox( constraints: constraints, @@ -133,27 +141,11 @@ class WebStripeCardState extends State with CardFieldContext { ); } - String _getBrandonTextFontCssSrc() { - final isHttps = Uri.base.scheme == 'https'; - if (isHttps) { - debugPrint('Loaded font from local assets'); - return Uri.base.origin + '/fonts.css'; - } else { - debugPrint( - 'You are currently using this application in a non secure environment (http). In order to load the correct font you should use a secure environment (https) OR if localhost use the arugment "--web-browser-flag=--disable-web-security" when running "flutter run" or the fonts will not load.'); - return 'https://workplace.insighttimer.com/fonts.css'; - } - } - js.JsElementsCreateOptions createElementOptions() { final textColor = widget.style?.textColor; + final fonts = widget.webFonts?.map((webFont) => webFont.toFont()).toList() ?? []; return js.JsElementsCreateOptions( - fonts: [ - Font( - family: 'Brandon Text', - cssSrc: _getBrandonTextFontCssSrc(), - ) - ], + fonts: fonts, appearance: js.jsify( js.ElementAppearance( theme: js.ElementTheme.stripe, @@ -227,3 +219,17 @@ class WebStripeCardState extends State with CardFieldContext { throw UnimplementedError(); } } + +extension on WebFont { + Font toFont() { + return Font( + family: family, + src: src ?? '', + display: display ?? '', + style: style ?? '', + unicodeRange: unicodeRange ?? '', + weight: weight ?? '', + cssSrc: cssSrc ?? '', + ); + } +}