From 4b0e4a98de1914c8d96c09f4f33a3ed664357f74 Mon Sep 17 00:00:00 2001 From: Nathan Anderson <nathananderson98@gmail.com> Date: Wed, 22 Nov 2023 10:52:52 -0700 Subject: [PATCH] Added web fix for platform call and static analysis fixes --- example/lib/dark_customization.dart | 11 +- example/lib/main.dart | 8 +- lib/card_details.dart | 28 ++- lib/card_provider_icon.dart | 11 +- lib/stripe_native_card_field.dart | 321 ++++++++++++++++-------- test/stripe_native_card_field_test.dart | 34 ++- 6 files changed, 278 insertions(+), 135 deletions(-) diff --git a/example/lib/dark_customization.dart b/example/lib/dark_customization.dart index 623f3cf..b59975d 100644 --- a/example/lib/dark_customization.dart +++ b/example/lib/dark_customization.dart @@ -1,4 +1,3 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stripe_native_card_field/card_details.dart'; @@ -19,7 +18,8 @@ class MyApp extends StatelessWidget { title: 'Native Stripe Field Demo', theme: ThemeData( brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, brightness: Brightness.dark), + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, brightness: Brightness.dark), useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), @@ -66,9 +66,10 @@ class _MyHomePageState extends State<MyHomePage> { print(details); } }, - textStyle: TextStyle(fontFamily: 'Lato', color: Colors.tealAccent), - hintTextStyle: TextStyle(fontFamily: 'Lato', color: Colors.teal), - errorTextStyle: TextStyle(color: Colors.purpleAccent), + textStyle: + const TextStyle(fontFamily: 'Lato', color: Colors.tealAccent), + hintTextStyle: const TextStyle(fontFamily: 'Lato', color: Colors.teal), + errorTextStyle: const TextStyle(color: Colors.purpleAccent), boxDecoration: BoxDecoration( color: Colors.black54, border: Border.all( diff --git a/example/lib/main.dart b/example/lib/main.dart index 6fa38c3..20b0c67 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,7 +41,7 @@ class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( - body: Center( + body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ @@ -58,12 +58,6 @@ class _MyHomePageState extends State<MyHomePage> { print(details); } }, - // textStyle: TextStyle(fontSize: 24.0), - // cardFieldWidth: 260, - // expFieldWidth: 100.0, - // securityFieldWidth: 60.0, - // postalFieldWidth: 130.0, - // iconSize: Size(50.0, 35.0), overrideValidState: state, errorText: errorText, ), diff --git a/lib/card_details.dart b/lib/card_details.dart index 7e94af8..f97ef78 100644 --- a/lib/card_details.dart +++ b/lib/card_details.dart @@ -18,7 +18,11 @@ class CardDetails { /// Sets every field to null, a default /// `CardDetails` when nothing has been entered. factory CardDetails.blank() { - return CardDetails(cardNumber: null, securityCode: null, expirationString: null, postalCode: null); + return CardDetails( + cardNumber: null, + securityCode: null, + expirationString: null, + postalCode: null); } /// Returns the CardNumber as a `String` with the spaces removed. @@ -51,8 +55,9 @@ class CardDetails { /// Returns true if `_cardNumber` is null, or /// if the _cardNumber matches the detected `provider`'s /// card lenght, defaulting to 16. - bool get cardNumberFilled => - _cardNumber == null ? false : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length; + bool get cardNumberFilled => _cardNumber == null + ? false + : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length; /// Returns true if all details are complete and valid /// otherwise, return false. @@ -78,7 +83,10 @@ class CardDetails { } _lastCheckHash = currentHash; - if (_cardNumber == null && expirationString == null && securityCode == null && postalCode == null) { + if (_cardNumber == null && + expirationString == null && + securityCode == null && + postalCode == null) { _complete = false; _validState = CardDetailsValidState.blank; return; @@ -111,7 +119,8 @@ class CardDetails { _validState = CardDetailsValidState.missingDate; return; } - final month = int.parse(expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first); + final month = int.parse( + expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first); if (month < 1 || month > 12) { _complete = false; _validState = CardDetailsValidState.invalidMonth; @@ -123,7 +132,8 @@ class CardDetails { _complete = false; _validState = CardDetailsValidState.dateTooEarly; return; - } else if (date.isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) { + } else if (date + .isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) { _complete = false; _validState = CardDetailsValidState.dateTooLate; return; @@ -282,7 +292,11 @@ class CardProvider { int cvcLength; CardProvider( - {required this.id, required this.cardLength, required this.cvcLength, this.innValidNums, this.innValidRanges}) { + {required this.id, + required this.cardLength, + required this.cvcLength, + this.innValidNums, + this.innValidRanges}) { // Must provide one or the other assert(innValidNums != null || innValidRanges != null); // Do not provide empty list of valid nums diff --git a/lib/card_provider_icon.dart b/lib/card_provider_icon.dart index 98f2e3d..a8a5052 100644 --- a/lib/card_provider_icon.dart +++ b/lib/card_provider_icon.dart @@ -7,7 +7,12 @@ import 'package:flutter_svg/flutter_svg.dart'; /// /// To see a list of supported card providers, see `CardDetails.provider`. class CardProviderIcon extends StatefulWidget { - const CardProviderIcon({required this.cardDetails, this.size, this.defaultCardColor, this.errorCardColor, super.key}); + const CardProviderIcon( + {required this.cardDetails, + this.size, + this.defaultCardColor, + this.errorCardColor, + super.key}); final CardDetails? cardDetails; final Size? size; @@ -29,9 +34,9 @@ class _CardProviderIconState extends State<CardProviderIcon> { cardProviderSvg = { 'credit-card': - '<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 15C7.55228 15 8 14.5523 8 14C8 13.4477 7.55228 13 7 13C6.44772 13 6 13.4477 6 14C6 14.5523 6.44772 15 7 15Z" fill="${defaultCardColor}" stroke="${defaultCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9V5.6C2 5.26863 2.26863 5 2.6 5H21.4C21.7314 5 22 5.26863 22 5.6V9M2 9V18.4C2 18.7314 2.26863 19 2.6 19H21.4C21.7314 19 22 18.7314 22 18.4V9M2 9H22" stroke="${defaultCardColor}" stroke-linecap="round" stroke-linejoin="round"/></svg>', + '<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 15C7.55228 15 8 14.5523 8 14C8 13.4477 7.55228 13 7 13C6.44772 13 6 13.4477 6 14C6 14.5523 6.44772 15 7 15Z" fill="$defaultCardColor" stroke="$defaultCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9V5.6C2 5.26863 2.26863 5 2.6 5H21.4C21.7314 5 22 5.26863 22 5.6V9M2 9V18.4C2 18.7314 2.26863 19 2.6 19H21.4C21.7314 19 22 18.7314 22 18.4V9M2 9H22" stroke="$defaultCardColor" stroke-linecap="round" stroke-linejoin="round"/></svg>', 'error': - '<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3L21 21" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 15C7.55228 15 8 14.5523 8 14C8 13.4477 7.55228 13 7 13C6.44772 13 6 13.4477 6 14C6 14.5523 6.44772 15 7 15Z" fill="${errorCardColor}" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 19H2.6C2.26863 19 2 18.7314 2 18.4V9H8.5" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9V5.6C2 5.26863 2.26863 5 2.6 5H4.5" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 9H22V17" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 9V5.6C22 5.26863 21.7314 5 21.4 5H10" stroke="${errorCardColor}" stroke-linecap="round" stroke-linejoin="round"/></svg>', + '<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3L21 21" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 15C7.55228 15 8 14.5523 8 14C8 13.4477 7.55228 13 7 13C6.44772 13 6 13.4477 6 14C6 14.5523 6.44772 15 7 15Z" fill="$errorCardColor" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 19H2.6C2.26863 19 2 18.7314 2 18.4V9H8.5" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9V5.6C2 5.26863 2.26863 5 2.6 5H4.5" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 9H22V17" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 9V5.6C22 5.26863 21.7314 5 21.4 5H10" stroke="$errorCardColor" stroke-linecap="round" stroke-linejoin="round"/></svg>', CardProviderID.discoverCard.name: '<svg height="500" viewBox="0 0 780 500" width="780" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="m54.992 0c-30.365 0-54.992 24.63-54.992 55.004v390.992c0 30.38 24.619 55.004 54.992 55.004h670.016c30.365 0 54.992-24.63 54.992-55.004v-390.992c0-30.38-24.619-55.004-54.992-55.004z" fill="#4d4d4d"/><path d="m327.152 161.893c8.837 0 16.248 1.784 25.268 6.09v22.751c-8.544-7.863-15.955-11.154-25.756-11.154-19.264 0-34.414 15.015-34.414 34.05 0 20.075 14.681 34.196 35.37 34.196 9.312 0 16.586-3.12 24.8-10.857v22.763c-9.341 4.14-16.911 5.776-25.756 5.776-31.278 0-55.582-22.596-55.582-51.737 0-28.826 24.951-51.878 56.07-51.878zm-97.113.627c11.546 0 22.11 3.72 30.943 10.994l-10.748 13.248c-5.35-5.646-10.41-8.028-16.564-8.028-8.853 0-15.3 4.745-15.3 10.989 0 5.354 3.619 8.188 15.944 12.482 23.365 8.044 30.29 15.176 30.29 30.926 0 19.193-14.976 32.553-36.32 32.553-15.63 0-26.994-5.795-36.458-18.872l13.268-12.03c4.73 8.61 12.622 13.222 22.42 13.222 9.163 0 15.947-5.952 15.947-13.984 0-4.164-2.055-7.734-6.158-10.258-2.066-1.195-6.158-2.977-14.2-5.647-19.291-6.538-25.91-13.527-25.91-27.185 0-16.225 14.214-28.41 32.846-28.41zm234.723 1.728h22.437l28.084 66.592 28.446-66.592h22.267l-45.494 101.686h-11.053zm-397.348.152h30.15c33.312 0 56.534 20.382 56.534 49.641 0 14.59-7.104 28.696-19.118 38.057-10.108 7.901-21.626 11.445-37.574 11.445h-29.992zm96.135 0h20.54v99.143h-20.54zm411.734 0h58.252v16.8h-37.725v22.005h36.336v16.791h-36.336v26.762h37.726v16.785h-58.252v-99.143zm71.858 0h30.455c23.69 0 37.265 10.71 37.265 29.272 0 15.18-8.514 25.14-23.986 28.105l33.148 41.766h-25.26l-28.429-39.828h-2.678v39.828h-20.515zm20.515 15.616v30.025h6.002c13.117 0 20.069-5.362 20.069-15.328 0-9.648-6.954-14.697-19.745-14.697zm-579.716 1.183v65.559h5.512c13.273 0 21.656-2.394 28.11-7.88 7.103-5.955 11.376-15.465 11.376-24.98 0-9.499-4.273-18.725-11.376-24.681-6.785-5.78-14.837-8.018-28.11-8.018z" fill="#fff"/><path d="m415.13 161.21c30.941 0 56.022 23.58 56.022 52.709v.033c0 29.13-25.081 52.742-56.021 52.742s-56.022-23.613-56.022-52.742v-.033c0-29.13 25.082-52.71 56.022-52.71zm364.85 127.15c-26.05 18.33-221.08 149.34-558.75 212.62h503.76c30.365 0 54.992-24.63 54.992-55.004v-157.62z" fill="#f47216"/></g></svg>', CardProviderID.americanExpress.name: diff --git a/lib/stripe_native_card_field.dart b/lib/stripe_native_card_field.dart index e84dce0..551e4a3 100644 --- a/lib/stripe_native_card_field.dart +++ b/lib/stripe_native_card_field.dart @@ -59,7 +59,8 @@ class CardTextField extends StatefulWidget { assert(stripePublishableKey!.startsWith('pk_')); if (kReleaseMode && !stripePublishableKey!.startsWith('pk_live_')) { log('StripeNativeCardField: *WARN* You are not using a live publishableKey in production.'); - } else if ((kDebugMode || kProfileMode) && stripePublishableKey!.startsWith('pk_live_')) { + } else if ((kDebugMode || kProfileMode) && + stripePublishableKey!.startsWith('pk_live_')) { log('StripeNativeCardField: *WARN* You are using a live stripe key in a debug environment, proceed with caution!'); log('StripeNativeCardField: *WARN* Ideally you should be using your test keys whenever not in production.'); } @@ -198,7 +199,7 @@ class CardTextFieldState extends State<CardTextField> { String? _validationErrorText; bool _showBorderError = false; - final _isMobile = Platform.isAndroid || Platform.isIOS; + final _isMobile = kIsWeb ? false : Platform.isAndroid || Platform.isIOS; /// If a request to Stripe is being made bool _loading = false; @@ -220,9 +221,12 @@ class CardTextFieldState extends State<CardTextField> { // No way to get backspace events on soft keyboards, so add invisible character to detect delete _cardNumberController = TextEditingController(); - _expirationController = TextEditingController(text: _isMobile ? '\u200b' : ''); - _securityCodeController = TextEditingController(text: _isMobile ? '\u200b' : ''); - _postalCodeController = TextEditingController(text: _isMobile ? '\u200b' : ''); + _expirationController = + TextEditingController(text: _isMobile ? '\u200b' : ''); + _securityCodeController = + TextEditingController(text: _isMobile ? '\u200b' : ''); + _postalCodeController = + TextEditingController(text: _isMobile ? '\u200b' : ''); // Otherwise, use `RawKeyboard` listener if (!_isMobile) { @@ -234,11 +238,15 @@ class CardTextFieldState extends State<CardTextField> { securityCodeFocusNode = FocusNode(); postalCodeFocusNode = FocusNode(); - _errorTextStyle = const TextStyle(color: Colors.red, fontSize: 14, inherit: true) - .merge(widget.errorTextStyle ?? widget.textStyle); - _normalTextStyle = const TextStyle(color: Colors.black87, fontSize: 14, inherit: true).merge(widget.textStyle); - _hintTextSyle = const TextStyle(color: Colors.black54, fontSize: 14, inherit: true) - .merge(widget.hintTextStyle ?? widget.textStyle); + _errorTextStyle = + const TextStyle(color: Colors.red, fontSize: 14, inherit: true) + .merge(widget.errorTextStyle ?? widget.textStyle); + _normalTextStyle = + const TextStyle(color: Colors.black87, fontSize: 14, inherit: true) + .merge(widget.textStyle); + _hintTextSyle = + const TextStyle(color: Colors.black54, fontSize: 14, inherit: true) + .merge(widget.hintTextStyle ?? widget.textStyle); _normalBoxDecoration = BoxDecoration( color: const Color(0xfff6f9fc), @@ -280,15 +288,31 @@ class CardTextFieldState extends State<CardTextField> { _onStepChange, ); - isWideFormat = - widget.width >= _cardFieldWidth + _expirationFieldWidth + _securityFieldWidth + _postalFieldWidth + 60.0; + isWideFormat = widget.width >= + _cardFieldWidth + + _expirationFieldWidth + + _securityFieldWidth + + _postalFieldWidth + + 60.0; if (isWideFormat) { _internalFieldWidth = widget.width + _postalFieldWidth + 35; - _expanderWidthExpanded = widget.width - _cardFieldWidth - _expirationFieldWidth - _securityFieldWidth - 35; - _expanderWidthCollapsed = - widget.width - _cardFieldWidth - _expirationFieldWidth - _securityFieldWidth - _postalFieldWidth - 70; + _expanderWidthExpanded = widget.width - + _cardFieldWidth - + _expirationFieldWidth - + _securityFieldWidth - + 35; + _expanderWidthCollapsed = widget.width - + _cardFieldWidth - + _expirationFieldWidth - + _securityFieldWidth - + _postalFieldWidth - + 70; } else { - _internalFieldWidth = _cardFieldWidth + _expirationFieldWidth + _securityFieldWidth + _postalFieldWidth + 80; + _internalFieldWidth = _cardFieldWidth + + _expirationFieldWidth + + _securityFieldWidth + + _postalFieldWidth + + 80; } super.initState(); @@ -314,8 +338,10 @@ class CardTextFieldState extends State<CardTextField> { @override Widget build(BuildContext context) { if ((widget.errorText != null || widget.overrideValidState != null) && - Object.hashAll([widget.errorText, widget.overrideValidState]) != _prevErrorOverrideHash) { - _prevErrorOverrideHash = Object.hashAll([widget.errorText, widget.overrideValidState]); + Object.hashAll([widget.errorText, widget.overrideValidState]) != + _prevErrorOverrideHash) { + _prevErrorOverrideHash = + Object.hashAll([widget.errorText, widget.overrideValidState]); _validateFields(); } return Column( @@ -332,9 +358,11 @@ class CardTextFieldState extends State<CardTextField> { // Enable scrolling on mobile and if its narrow (not all fields visible) onHorizontalDragUpdate: (details) { const minOffset = 0.0; - final maxOffset = _horizontalScrollController.position.maxScrollExtent; + final maxOffset = + _horizontalScrollController.position.maxScrollExtent; if (!_isMobile || isWideFormat) return; - final newOffset = _horizontalScrollController.offset - details.delta.dx; + final newOffset = + _horizontalScrollController.offset - details.delta.dx; if (newOffset < minOffset) { _horizontalScrollController.jumpTo(minOffset); @@ -345,19 +373,24 @@ class CardTextFieldState extends State<CardTextField> { } }, onHorizontalDragEnd: (details) { - if (!_isMobile || isWideFormat || details.primaryVelocity == null) return; + if (!_isMobile || isWideFormat || details.primaryVelocity == null) { + return; + } const dur = Duration(milliseconds: 300); const cur = Curves.ease; // final max = _horizontalScrollController.position.maxScrollExtent; - final newOffset = _horizontalScrollController.offset - details.primaryVelocity! * 0.15; - _horizontalScrollController.animateTo(newOffset, curve: cur, duration: dur); + final newOffset = _horizontalScrollController.offset - + details.primaryVelocity! * 0.15; + _horizontalScrollController.animateTo(newOffset, + curve: cur, duration: dur); }, child: Container( width: widget.width, height: widget.height ?? 60.0, - decoration: _showBorderError ? _errorBoxDecoration : _normalBoxDecoration, + decoration: + _showBorderError ? _errorBoxDecoration : _normalBoxDecoration, child: ClipRect( child: IgnorePointer( child: SingleChildScrollView( @@ -371,7 +404,8 @@ class CardTextFieldState extends State<CardTextField> { mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), + padding: + const EdgeInsets.symmetric(horizontal: 6.0), child: CardProviderIcon( cardDetails: _cardDetails, size: widget.iconSize, @@ -398,27 +432,36 @@ class CardTextFieldState extends State<CardTextField> { return null; } _cardDetails.cardNumber = content; - if (_cardDetails.validState == CardDetailsValidState.invalidCard) { - _setValidationState('Your card number is invalid.'); - } else if (_cardDetails.validState == CardDetailsValidState.missingCard) { - _setValidationState('Your card number is incomplete.'); + if (_cardDetails.validState == + CardDetailsValidState.invalidCard) { + _setValidationState( + 'Your card number is invalid.'); + } else if (_cardDetails.validState == + CardDetailsValidState.missingCard) { + _setValidationState( + 'Your card number is incomplete.'); } return null; }, onChanged: (str) { final numbers = str.replaceAll(' ', ''); - setState(() => _cardDetails.cardNumber = numbers); + setState( + () => _cardDetails.cardNumber = numbers); if (str.length <= _cardDetails.maxINNLength) { _cardDetails.detectCardProvider(); } if (numbers.length == 16) { - _currentCardEntryStepController.add(CardEntryStep.exp); + _currentCardEntryStepController + .add(CardEntryStep.exp); } }, - onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.exp), + onFieldSubmitted: (_) => + _currentCardEntryStepController + .add(CardEntryStep.exp), inputFormatters: [ LengthLimitingTextInputFormatter(19), - FilteringTextInputFormatter.allow(RegExp('[0-9 ]')), + FilteringTextInputFormatter.allow( + RegExp('[0-9 ]')), CardNumberInputFormatter(), ], decoration: InputDecoration( @@ -437,13 +480,14 @@ class CardTextFieldState extends State<CardTextField> { child: AnimatedContainer( curve: Curves.easeInOut, duration: const Duration(milliseconds: 400), - constraints: _currentStep == CardEntryStep.number - ? BoxConstraints.loose( - Size(_expanderWidthExpanded, 0.0), - ) - : BoxConstraints.tight( - Size(_expanderWidthCollapsed, 0.0), - ), + constraints: + _currentStep == CardEntryStep.number + ? BoxConstraints.loose( + Size(_expanderWidthExpanded, 0.0), + ) + : BoxConstraints.tight( + Size(_expanderWidthCollapsed, 0.0), + ), ), ), @@ -454,7 +498,8 @@ class CardTextFieldState extends State<CardTextField> { alignment: Alignment.centerLeft, children: [ // Must manually add hint label because they wont show on mobile with backspace hack - if (_isMobile && _expirationController.text == '\u200b') + if (_isMobile && + _expirationController.text == '\u200b') Text('MM/YY', style: _hintTextSyle), TextFormField( key: const Key('expiration_field'), @@ -470,24 +515,37 @@ class CardTextFieldState extends State<CardTextField> { ? _errorTextStyle : _normalTextStyle, validator: (content) { - if (content == null || content.isEmpty || _isMobile && content == '\u200b') { + if (content == null || + content.isEmpty || + _isMobile && content == '\u200b') { return null; } if (_isMobile) { - setState(() => _cardDetails.expirationString = content.replaceAll('\u200b', '')); + setState(() => + _cardDetails.expirationString = + content.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.expirationString = content); + setState(() => _cardDetails + .expirationString = content); } - if (_cardDetails.validState == CardDetailsValidState.dateTooEarly) { - _setValidationState('Your card\'s expiration date is in the past.'); - } else if (_cardDetails.validState == CardDetailsValidState.dateTooLate) { - _setValidationState('Your card\'s expiration year is invalid.'); - } else if (_cardDetails.validState == CardDetailsValidState.missingDate) { - _setValidationState('You must include your card\'s expiration date.'); - } else if (_cardDetails.validState == CardDetailsValidState.invalidMonth) { - _setValidationState('Your card\'s expiration month is invalid.'); + if (_cardDetails.validState == + CardDetailsValidState.dateTooEarly) { + _setValidationState( + 'Your card\'s expiration date is in the past.'); + } else if (_cardDetails.validState == + CardDetailsValidState.dateTooLate) { + _setValidationState( + 'Your card\'s expiration year is invalid.'); + } else if (_cardDetails.validState == + CardDetailsValidState.missingDate) { + _setValidationState( + 'You must include your card\'s expiration date.'); + } else if (_cardDetails.validState == + CardDetailsValidState.invalidMonth) { + _setValidationState( + 'Your card\'s expiration month is invalid.'); } return null; }, @@ -496,18 +554,25 @@ class CardTextFieldState extends State<CardTextField> { if (str.isEmpty) { _backspacePressed(); } - setState(() => _cardDetails.expirationString = str.replaceAll('\u200b', '')); + setState(() => + _cardDetails.expirationString = + str.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.expirationString = str); + setState(() => + _cardDetails.expirationString = str); } if (str.length == 5) { - _currentCardEntryStepController.add(CardEntryStep.cvc); + _currentCardEntryStepController + .add(CardEntryStep.cvc); } }, - onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.cvc), + onFieldSubmitted: (_) => + _currentCardEntryStepController + .add(CardEntryStep.cvc), inputFormatters: [ LengthLimitingTextInputFormatter(5), - FilteringTextInputFormatter.allow(RegExp('[0-9/]')), + FilteringTextInputFormatter.allow( + RegExp('[0-9/]')), CardExpirationFormatter(), ], decoration: InputDecoration( @@ -526,7 +591,8 @@ class CardTextFieldState extends State<CardTextField> { child: Stack( alignment: Alignment.centerLeft, children: [ - if (_isMobile && _securityCodeController.text == '\u200b') + if (_isMobile && + _securityCodeController.text == '\u200b') Text( 'CVC', style: _hintTextSyle, @@ -536,47 +602,67 @@ class CardTextFieldState extends State<CardTextField> { focusNode: securityCodeFocusNode, controller: _securityCodeController, keyboardType: TextInputType.number, - style: - _isRedText([CardDetailsValidState.invalidCVC, CardDetailsValidState.missingCVC]) - ? _errorTextStyle - : _normalTextStyle, + style: _isRedText([ + CardDetailsValidState.invalidCVC, + CardDetailsValidState.missingCVC + ]) + ? _errorTextStyle + : _normalTextStyle, validator: (content) { - if (content == null || content.isEmpty || _isMobile && content == '\u200b') { + if (content == null || + content.isEmpty || + _isMobile && content == '\u200b') { return null; } if (_isMobile) { - setState(() => _cardDetails.securityCode = content.replaceAll('\u200b', '')); + setState(() => _cardDetails.securityCode = + content.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.securityCode = content); + setState(() => + _cardDetails.securityCode = content); } - if (_cardDetails.validState == CardDetailsValidState.invalidCVC) { - _setValidationState('Your card\'s security code is invalid.'); - } else if (_cardDetails.validState == CardDetailsValidState.missingCVC) { - _setValidationState('Your card\'s security code is incomplete.'); + if (_cardDetails.validState == + CardDetailsValidState.invalidCVC) { + _setValidationState( + 'Your card\'s security code is invalid.'); + } else if (_cardDetails.validState == + CardDetailsValidState.missingCVC) { + _setValidationState( + 'Your card\'s security code is incomplete.'); } return null; }, - onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.postal), + onFieldSubmitted: (_) => + _currentCardEntryStepController + .add(CardEntryStep.postal), onChanged: (str) { if (_isMobile) { if (str.isEmpty) { _backspacePressed(); } - setState(() => _cardDetails.expirationString = str.replaceAll('\u200b', '')); + setState(() => + _cardDetails.expirationString = + str.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.expirationString = str); + setState(() => + _cardDetails.expirationString = str); } - if (str.length == _cardDetails.provider?.cvcLength) { - _currentCardEntryStepController.add(CardEntryStep.postal); + if (str.length == + _cardDetails.provider?.cvcLength) { + _currentCardEntryStepController + .add(CardEntryStep.postal); } }, inputFormatters: [ LengthLimitingTextInputFormatter( - _cardDetails.provider == null ? 4 : _cardDetails.provider!.cvcLength), - FilteringTextInputFormatter.allow(RegExp('[0-9]')), + _cardDetails.provider == null + ? 4 + : _cardDetails.provider!.cvcLength), + FilteringTextInputFormatter.allow( + RegExp('[0-9]')), ], decoration: InputDecoration( contentPadding: EdgeInsets.zero, @@ -594,7 +680,8 @@ class CardTextFieldState extends State<CardTextField> { child: Stack( alignment: Alignment.centerLeft, children: [ - if (_isMobile && _postalCodeController.text == '\u200b') + if (_isMobile && + _postalCodeController.text == '\u200b') Text( 'Postal Code', style: _hintTextSyle, @@ -604,25 +691,35 @@ class CardTextFieldState extends State<CardTextField> { focusNode: postalCodeFocusNode, controller: _postalCodeController, keyboardType: TextInputType.number, - style: - _isRedText([CardDetailsValidState.invalidZip, CardDetailsValidState.missingZip]) - ? _errorTextStyle - : _normalTextStyle, + style: _isRedText([ + CardDetailsValidState.invalidZip, + CardDetailsValidState.missingZip + ]) + ? _errorTextStyle + : _normalTextStyle, validator: (content) { - if (content == null || content.isEmpty || _isMobile && content == '\u200b') { + if (content == null || + content.isEmpty || + _isMobile && content == '\u200b') { return null; } if (_isMobile) { - setState(() => _cardDetails.postalCode = content.replaceAll('\u200b', '')); + setState(() => _cardDetails.postalCode = + content.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.postalCode = content); + setState(() => + _cardDetails.postalCode = content); } - if (_cardDetails.validState == CardDetailsValidState.invalidZip) { - _setValidationState('The postal code you entered is not correct.'); - } else if (_cardDetails.validState == CardDetailsValidState.missingZip) { - _setValidationState('You must enter your card\'s postal code.'); + if (_cardDetails.validState == + CardDetailsValidState.invalidZip) { + _setValidationState( + 'The postal code you entered is not correct.'); + } else if (_cardDetails.validState == + CardDetailsValidState.missingZip) { + _setValidationState( + 'You must enter your card\'s postal code.'); } return null; }, @@ -631,9 +728,11 @@ class CardTextFieldState extends State<CardTextField> { if (str.isEmpty) { _backspacePressed(); } - setState(() => _cardDetails.postalCode = str.replaceAll('\u200b', '')); + setState(() => _cardDetails.postalCode = + str.replaceAll('\u200b', '')); } else { - setState(() => _cardDetails.postalCode = str); + setState( + () => _cardDetails.postalCode = str); } }, textInputAction: TextInputAction.done, @@ -653,8 +752,12 @@ class CardTextFieldState extends State<CardTextField> { ), AnimatedOpacity( duration: const Duration(milliseconds: 300), - opacity: _loading && widget.showInternalLoadingWidget ? 1.0 : 0.0, - child: widget.loadingWidget ?? const CircularProgressIndicator(), + opacity: + _loading && widget.showInternalLoadingWidget + ? 1.0 + : 0.0, + child: widget.loadingWidget ?? + const CircularProgressIndicator(), ), ], ), @@ -762,14 +865,20 @@ class CardTextFieldState extends State<CardTextField> { _horizontalScrollController.animateTo(0.0, duration: dur, curve: cur); break; case CardEntryStep.exp: - _horizontalScrollController.animateTo(_cardFieldWidth / 2, duration: dur, curve: cur); + _horizontalScrollController.animateTo(_cardFieldWidth / 2, + duration: dur, curve: cur); break; case CardEntryStep.cvc: - _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth, duration: dur, curve: cur); + _horizontalScrollController.animateTo( + _cardFieldWidth / 2 + _expirationFieldWidth, + duration: dur, + curve: cur); break; case CardEntryStep.postal: - _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth, - duration: dur, curve: cur); + _horizontalScrollController.animateTo( + _cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth, + duration: dur, + curve: cur); break; } } @@ -842,11 +951,11 @@ class CardTextFieldState extends State<CardTextField> { case CardEntryStep.number: break; case CardEntryStep.exp: - if (_expirationController.text.isNotEmpty) break; + if (_expirationController.text.isNotEmpty) return; case CardEntryStep.cvc: - if (_securityCodeController.text.isNotEmpty) break; + if (_securityCodeController.text.isNotEmpty) return; case CardEntryStep.postal: - if (_postalCodeController.text.isNotEmpty) break; + if (_postalCodeController.text.isNotEmpty) return; } _transitionStepFocus(); } @@ -893,7 +1002,8 @@ class CardTextFieldState extends State<CardTextField> { /// to make the card number display cleanly. class CardNumberInputFormatter implements TextInputFormatter { @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { String cardNum = newValue.text; if (cardNum.length <= 4) return newValue; @@ -908,7 +1018,9 @@ class CardNumberInputFormatter implements TextInputFormatter { } } - return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length)); + return newValue.copyWith( + text: buffer.toString(), + selection: TextSelection.collapsed(offset: buffer.length)); } } @@ -916,7 +1028,8 @@ class CardNumberInputFormatter implements TextInputFormatter { /// the month and the year for the expiration date. class CardExpirationFormatter implements TextInputFormatter { @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { String cardExp = newValue.text; if (cardExp.length == 1) { if (cardExp[0] == '0' || cardExp[0] == '1') { @@ -937,6 +1050,8 @@ class CardExpirationFormatter implements TextInputFormatter { buffer.write('/'); } } - return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length)); + return newValue.copyWith( + text: buffer.toString(), + selection: TextSelection.collapsed(offset: buffer.length)); } } diff --git a/test/stripe_native_card_field_test.dart b/test/stripe_native_card_field_test.dart index 1dcd74d..0e451dc 100644 --- a/test/stripe_native_card_field_test.dart +++ b/test/stripe_native_card_field_test.dart @@ -24,7 +24,8 @@ void main() { final input = TestTextInput(); - final cardState = tester.state(find.byType(CardTextField)) as CardTextFieldState; + final cardState = + tester.state(find.byType(CardTextField)) as CardTextFieldState; assertEmptyTextFields(tester, cardState.isWideFormat); @@ -47,7 +48,8 @@ void main() { await tester.pump(); expect(getTextFormField(expirationFieldKey).controller?.text, ''); - expect(getTextFormField(cardFieldKey).controller?.text, '4242 4242 4242 424'); + expect(getTextFormField(cardFieldKey).controller?.text, + '4242 4242 4242 424'); expect(cardState.cardNumberFocusNode.hasFocus, true); expect(cardState.expirationFocusNode.hasFocus, false); // Postal code should now be gone @@ -58,7 +60,8 @@ void main() { input.enterText("4242424242424242"); await tester.pump(); - expect(getTextFormField(cardFieldKey).controller?.text, '4242 4242 4242 4242'); + expect(getTextFormField(cardFieldKey).controller?.text, + '4242 4242 4242 4242'); expect(cardState.cardNumberFocusNode.hasFocus, false); expect(cardState.expirationFocusNode.hasFocus, true); // Postal code should move back into view @@ -90,7 +93,10 @@ void main() { await tester.pump(); final expectedCardDetails = CardDetails( - cardNumber: '4242 4242 4242 4242', securityCode: '333', expirationString: '10/28', postalCode: '91555'); + cardNumber: '4242 4242 4242 4242', + securityCode: '333', + expirationString: '10/28', + postalCode: '91555'); // print('${expectedCardDetails.toString()}\n${details?.toString()}'); expect(details?.hash, expectedCardDetails.hash); }, @@ -110,7 +116,8 @@ void main() { final input = TestTextInput(); - final cardState = tester.state(find.byType(CardTextField)) as CardTextFieldState; + final cardState = + tester.state(find.byType(CardTextField)) as CardTextFieldState; assertEmptyTextFields(tester, cardState.isWideFormat); @@ -135,7 +142,8 @@ void main() { input.enterText('0055'); await tester.pump(); - expect(find.text("Your card's expiration month is invalid."), findsOneWidget); + expect( + find.text("Your card's expiration month is invalid."), findsOneWidget); await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace); await tester.pump(); @@ -145,7 +153,8 @@ void main() { input.enterText('1099'); await tester.pump(); - expect(find.text("Your card's expiration year is invalid."), findsOneWidget); + expect( + find.text("Your card's expiration year is invalid."), findsOneWidget); await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace); await tester.pump(); @@ -169,7 +178,8 @@ void main() { await input.receiveAction(TextInputAction.done); await tester.pump(); - expect(find.text("The postal code you entered is not correct."), findsOneWidget); + expect(find.text("The postal code you entered is not correct."), + findsOneWidget); await tester.tap(find.byType(CardTextField)); @@ -182,7 +192,10 @@ void main() { await tester.pump(); final expectedCardDetails = CardDetails( - cardNumber: '4242 4242 4242 4242', expirationString: '02/28', securityCode: '123', postalCode: '12345'); + cardNumber: '4242 4242 4242 4242', + expirationString: '02/28', + securityCode: '123', + postalCode: '12345'); expect(details?.hash, expectedCardDetails.hash); }); @@ -209,7 +222,8 @@ void assertEmptyTextFields(WidgetTester tester, bool isWideFormat) { // expect(find.text("Postal Code"), findsNothing); } -Future<void> enterTextByKey(WidgetTester tester, {required String key, required String text}) async { +Future<void> enterTextByKey(WidgetTester tester, + {required String key, required String text}) async { await tester.enterText(find.byKey(ValueKey(key)), text); }