Compare commits

..

No commits in common. "1c918f72f97283d4d2ee0e7e57646d85dd2297e4" and "645d0749da1f6aa0cb4a71d593887878215ee933" have entirely different histories.

7 changed files with 135 additions and 283 deletions

View File

@ -1,8 +1,3 @@
## 0.0.5
- Fix Web, invalid call to `Platform.isAndroid`
- Analysis issues fixed for pub points
## 0.0.4 ## 0.0.4
- Fix for focus and soft keyboard on mobile devices - Fix for focus and soft keyboard on mobile devices

View File

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stripe_native_card_field/card_details.dart'; import 'package:stripe_native_card_field/card_details.dart';
@ -18,8 +19,7 @@ class MyApp extends StatelessWidget {
title: 'Native Stripe Field Demo', title: 'Native Stripe Field Demo',
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, brightness: Brightness.dark),
seedColor: Colors.deepPurple, brightness: Brightness.dark),
useMaterial3: true, useMaterial3: true,
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), home: const MyHomePage(title: 'Flutter Demo Home Page'),
@ -66,10 +66,9 @@ class _MyHomePageState extends State<MyHomePage> {
print(details); print(details);
} }
}, },
textStyle: textStyle: TextStyle(fontFamily: 'Lato', color: Colors.tealAccent),
const TextStyle(fontFamily: 'Lato', color: Colors.tealAccent), hintTextStyle: TextStyle(fontFamily: 'Lato', color: Colors.teal),
hintTextStyle: const TextStyle(fontFamily: 'Lato', color: Colors.teal), errorTextStyle: TextStyle(color: Colors.purpleAccent),
errorTextStyle: const TextStyle(color: Colors.purpleAccent),
boxDecoration: BoxDecoration( boxDecoration: BoxDecoration(
color: Colors.black54, color: Colors.black54,
border: Border.all( border: Border.all(

View File

@ -58,6 +58,12 @@ class _MyHomePageState extends State<MyHomePage> {
print(details); 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, overrideValidState: state,
errorText: errorText, errorText: errorText,
), ),

View File

@ -18,11 +18,7 @@ class CardDetails {
/// Sets every field to null, a default /// Sets every field to null, a default
/// `CardDetails` when nothing has been entered. /// `CardDetails` when nothing has been entered.
factory CardDetails.blank() { factory CardDetails.blank() {
return CardDetails( return CardDetails(cardNumber: null, securityCode: null, expirationString: null, postalCode: null);
cardNumber: null,
securityCode: null,
expirationString: null,
postalCode: null);
} }
/// Returns the CardNumber as a `String` with the spaces removed. /// Returns the CardNumber as a `String` with the spaces removed.
@ -55,9 +51,8 @@ class CardDetails {
/// Returns true if `_cardNumber` is null, or /// Returns true if `_cardNumber` is null, or
/// if the _cardNumber matches the detected `provider`'s /// if the _cardNumber matches the detected `provider`'s
/// card lenght, defaulting to 16. /// card lenght, defaulting to 16.
bool get cardNumberFilled => _cardNumber == null bool get cardNumberFilled =>
? false _cardNumber == null ? false : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length;
: (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length;
/// Returns true if all details are complete and valid /// Returns true if all details are complete and valid
/// otherwise, return false. /// otherwise, return false.
@ -83,10 +78,7 @@ class CardDetails {
} }
_lastCheckHash = currentHash; _lastCheckHash = currentHash;
if (_cardNumber == null && if (_cardNumber == null && expirationString == null && securityCode == null && postalCode == null) {
expirationString == null &&
securityCode == null &&
postalCode == null) {
_complete = false; _complete = false;
_validState = CardDetailsValidState.blank; _validState = CardDetailsValidState.blank;
return; return;
@ -119,8 +111,7 @@ class CardDetails {
_validState = CardDetailsValidState.missingDate; _validState = CardDetailsValidState.missingDate;
return; return;
} }
final month = int.parse( final month = int.parse(expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first);
expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first);
if (month < 1 || month > 12) { if (month < 1 || month > 12) {
_complete = false; _complete = false;
_validState = CardDetailsValidState.invalidMonth; _validState = CardDetailsValidState.invalidMonth;
@ -132,8 +123,7 @@ class CardDetails {
_complete = false; _complete = false;
_validState = CardDetailsValidState.dateTooEarly; _validState = CardDetailsValidState.dateTooEarly;
return; return;
} else if (date } else if (date.isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) {
.isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) {
_complete = false; _complete = false;
_validState = CardDetailsValidState.dateTooLate; _validState = CardDetailsValidState.dateTooLate;
return; return;
@ -292,11 +282,7 @@ class CardProvider {
int cvcLength; int cvcLength;
CardProvider( CardProvider(
{required this.id, {required this.id, required this.cardLength, required this.cvcLength, this.innValidNums, this.innValidRanges}) {
required this.cardLength,
required this.cvcLength,
this.innValidNums,
this.innValidRanges}) {
// Must provide one or the other // Must provide one or the other
assert(innValidNums != null || innValidRanges != null); assert(innValidNums != null || innValidRanges != null);
// Do not provide empty list of valid nums // Do not provide empty list of valid nums

View File

@ -7,12 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart';
/// ///
/// To see a list of supported card providers, see `CardDetails.provider`. /// To see a list of supported card providers, see `CardDetails.provider`.
class CardProviderIcon extends StatefulWidget { class CardProviderIcon extends StatefulWidget {
const CardProviderIcon( const CardProviderIcon({required this.cardDetails, this.size, this.defaultCardColor, this.errorCardColor, super.key});
{required this.cardDetails,
this.size,
this.defaultCardColor,
this.errorCardColor,
super.key});
final CardDetails? cardDetails; final CardDetails? cardDetails;
final Size? size; final Size? size;
@ -34,9 +29,9 @@ class _CardProviderIconState extends State<CardProviderIcon> {
cardProviderSvg = { cardProviderSvg = {
'credit-card': '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': '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: 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>', '<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: CardProviderID.americanExpress.name:

View File

@ -59,8 +59,7 @@ class CardTextField extends StatefulWidget {
assert(stripePublishableKey!.startsWith('pk_')); assert(stripePublishableKey!.startsWith('pk_'));
if (kReleaseMode && !stripePublishableKey!.startsWith('pk_live_')) { if (kReleaseMode && !stripePublishableKey!.startsWith('pk_live_')) {
log('StripeNativeCardField: *WARN* You are not using a live publishableKey in production.'); log('StripeNativeCardField: *WARN* You are not using a live publishableKey in production.');
} else if ((kDebugMode || kProfileMode) && } else if ((kDebugMode || kProfileMode) && stripePublishableKey!.startsWith('pk_live_')) {
stripePublishableKey!.startsWith('pk_live_')) {
log('StripeNativeCardField: *WARN* You are using a live stripe key in a debug environment, proceed with caution!'); 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.'); log('StripeNativeCardField: *WARN* Ideally you should be using your test keys whenever not in production.');
} }
@ -199,7 +198,7 @@ class CardTextFieldState extends State<CardTextField> {
String? _validationErrorText; String? _validationErrorText;
bool _showBorderError = false; bool _showBorderError = false;
final _isMobile = kIsWeb ? false : Platform.isAndroid || Platform.isIOS; final _isMobile = Platform.isAndroid || Platform.isIOS;
/// If a request to Stripe is being made /// If a request to Stripe is being made
bool _loading = false; bool _loading = false;
@ -221,12 +220,9 @@ class CardTextFieldState extends State<CardTextField> {
// No way to get backspace events on soft keyboards, so add invisible character to detect delete // No way to get backspace events on soft keyboards, so add invisible character to detect delete
_cardNumberController = TextEditingController(); _cardNumberController = TextEditingController();
_expirationController = _expirationController = TextEditingController(text: _isMobile ? '\u200b' : '');
TextEditingController(text: _isMobile ? '\u200b' : ''); _securityCodeController = TextEditingController(text: _isMobile ? '\u200b' : '');
_securityCodeController = _postalCodeController = TextEditingController(text: _isMobile ? '\u200b' : '');
TextEditingController(text: _isMobile ? '\u200b' : '');
_postalCodeController =
TextEditingController(text: _isMobile ? '\u200b' : '');
// Otherwise, use `RawKeyboard` listener // Otherwise, use `RawKeyboard` listener
if (!_isMobile) { if (!_isMobile) {
@ -238,14 +234,10 @@ class CardTextFieldState extends State<CardTextField> {
securityCodeFocusNode = FocusNode(); securityCodeFocusNode = FocusNode();
postalCodeFocusNode = FocusNode(); postalCodeFocusNode = FocusNode();
_errorTextStyle = _errorTextStyle = const TextStyle(color: Colors.red, fontSize: 14, inherit: true)
const TextStyle(color: Colors.red, fontSize: 14, inherit: true)
.merge(widget.errorTextStyle ?? widget.textStyle); .merge(widget.errorTextStyle ?? widget.textStyle);
_normalTextStyle = _normalTextStyle = const TextStyle(color: Colors.black87, fontSize: 14, inherit: true).merge(widget.textStyle);
const TextStyle(color: Colors.black87, fontSize: 14, inherit: true) _hintTextSyle = const TextStyle(color: Colors.black54, fontSize: 14, inherit: true)
.merge(widget.textStyle);
_hintTextSyle =
const TextStyle(color: Colors.black54, fontSize: 14, inherit: true)
.merge(widget.hintTextStyle ?? widget.textStyle); .merge(widget.hintTextStyle ?? widget.textStyle);
_normalBoxDecoration = BoxDecoration( _normalBoxDecoration = BoxDecoration(
@ -288,31 +280,15 @@ class CardTextFieldState extends State<CardTextField> {
_onStepChange, _onStepChange,
); );
isWideFormat = widget.width >= isWideFormat =
_cardFieldWidth + widget.width >= _cardFieldWidth + _expirationFieldWidth + _securityFieldWidth + _postalFieldWidth + 60.0;
_expirationFieldWidth +
_securityFieldWidth +
_postalFieldWidth +
60.0;
if (isWideFormat) { if (isWideFormat) {
_internalFieldWidth = widget.width + _postalFieldWidth + 35; _internalFieldWidth = widget.width + _postalFieldWidth + 35;
_expanderWidthExpanded = widget.width - _expanderWidthExpanded = widget.width - _cardFieldWidth - _expirationFieldWidth - _securityFieldWidth - 35;
_cardFieldWidth - _expanderWidthCollapsed =
_expirationFieldWidth - widget.width - _cardFieldWidth - _expirationFieldWidth - _securityFieldWidth - _postalFieldWidth - 70;
_securityFieldWidth -
35;
_expanderWidthCollapsed = widget.width -
_cardFieldWidth -
_expirationFieldWidth -
_securityFieldWidth -
_postalFieldWidth -
70;
} else { } else {
_internalFieldWidth = _cardFieldWidth + _internalFieldWidth = _cardFieldWidth + _expirationFieldWidth + _securityFieldWidth + _postalFieldWidth + 80;
_expirationFieldWidth +
_securityFieldWidth +
_postalFieldWidth +
80;
} }
super.initState(); super.initState();
@ -338,10 +314,8 @@ class CardTextFieldState extends State<CardTextField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if ((widget.errorText != null || widget.overrideValidState != null) && if ((widget.errorText != null || widget.overrideValidState != null) &&
Object.hashAll([widget.errorText, widget.overrideValidState]) != Object.hashAll([widget.errorText, widget.overrideValidState]) != _prevErrorOverrideHash) {
_prevErrorOverrideHash) { _prevErrorOverrideHash = Object.hashAll([widget.errorText, widget.overrideValidState]);
_prevErrorOverrideHash =
Object.hashAll([widget.errorText, widget.overrideValidState]);
_validateFields(); _validateFields();
} }
return Column( return Column(
@ -358,11 +332,9 @@ class CardTextFieldState extends State<CardTextField> {
// Enable scrolling on mobile and if its narrow (not all fields visible) // Enable scrolling on mobile and if its narrow (not all fields visible)
onHorizontalDragUpdate: (details) { onHorizontalDragUpdate: (details) {
const minOffset = 0.0; const minOffset = 0.0;
final maxOffset = final maxOffset = _horizontalScrollController.position.maxScrollExtent;
_horizontalScrollController.position.maxScrollExtent;
if (!_isMobile || isWideFormat) return; if (!_isMobile || isWideFormat) return;
final newOffset = final newOffset = _horizontalScrollController.offset - details.delta.dx;
_horizontalScrollController.offset - details.delta.dx;
if (newOffset < minOffset) { if (newOffset < minOffset) {
_horizontalScrollController.jumpTo(minOffset); _horizontalScrollController.jumpTo(minOffset);
@ -373,24 +345,19 @@ class CardTextFieldState extends State<CardTextField> {
} }
}, },
onHorizontalDragEnd: (details) { onHorizontalDragEnd: (details) {
if (!_isMobile || isWideFormat || details.primaryVelocity == null) { if (!_isMobile || isWideFormat || details.primaryVelocity == null) return;
return;
}
const dur = Duration(milliseconds: 300); const dur = Duration(milliseconds: 300);
const cur = Curves.ease; const cur = Curves.ease;
// final max = _horizontalScrollController.position.maxScrollExtent; // final max = _horizontalScrollController.position.maxScrollExtent;
final newOffset = _horizontalScrollController.offset - final newOffset = _horizontalScrollController.offset - details.primaryVelocity! * 0.15;
details.primaryVelocity! * 0.15; _horizontalScrollController.animateTo(newOffset, curve: cur, duration: dur);
_horizontalScrollController.animateTo(newOffset,
curve: cur, duration: dur);
}, },
child: Container( child: Container(
width: widget.width, width: widget.width,
height: widget.height ?? 60.0, height: widget.height ?? 60.0,
decoration: decoration: _showBorderError ? _errorBoxDecoration : _normalBoxDecoration,
_showBorderError ? _errorBoxDecoration : _normalBoxDecoration,
child: ClipRect( child: ClipRect(
child: IgnorePointer( child: IgnorePointer(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -404,8 +371,7 @@ class CardTextFieldState extends State<CardTextField> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 6.0),
const EdgeInsets.symmetric(horizontal: 6.0),
child: CardProviderIcon( child: CardProviderIcon(
cardDetails: _cardDetails, cardDetails: _cardDetails,
size: widget.iconSize, size: widget.iconSize,
@ -432,36 +398,27 @@ class CardTextFieldState extends State<CardTextField> {
return null; return null;
} }
_cardDetails.cardNumber = content; _cardDetails.cardNumber = content;
if (_cardDetails.validState == if (_cardDetails.validState == CardDetailsValidState.invalidCard) {
CardDetailsValidState.invalidCard) { _setValidationState('Your card number is invalid.');
_setValidationState( } else if (_cardDetails.validState == CardDetailsValidState.missingCard) {
'Your card number is invalid.'); _setValidationState('Your card number is incomplete.');
} else if (_cardDetails.validState ==
CardDetailsValidState.missingCard) {
_setValidationState(
'Your card number is incomplete.');
} }
return null; return null;
}, },
onChanged: (str) { onChanged: (str) {
final numbers = str.replaceAll(' ', ''); final numbers = str.replaceAll(' ', '');
setState( setState(() => _cardDetails.cardNumber = numbers);
() => _cardDetails.cardNumber = numbers);
if (str.length <= _cardDetails.maxINNLength) { if (str.length <= _cardDetails.maxINNLength) {
_cardDetails.detectCardProvider(); _cardDetails.detectCardProvider();
} }
if (numbers.length == 16) { if (numbers.length == 16) {
_currentCardEntryStepController _currentCardEntryStepController.add(CardEntryStep.exp);
.add(CardEntryStep.exp);
} }
}, },
onFieldSubmitted: (_) => onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.exp),
_currentCardEntryStepController
.add(CardEntryStep.exp),
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(19), LengthLimitingTextInputFormatter(19),
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(RegExp('[0-9 ]')),
RegExp('[0-9 ]')),
CardNumberInputFormatter(), CardNumberInputFormatter(),
], ],
decoration: InputDecoration( decoration: InputDecoration(
@ -480,8 +437,7 @@ class CardTextFieldState extends State<CardTextField> {
child: AnimatedContainer( child: AnimatedContainer(
curve: Curves.easeInOut, curve: Curves.easeInOut,
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
constraints: constraints: _currentStep == CardEntryStep.number
_currentStep == CardEntryStep.number
? BoxConstraints.loose( ? BoxConstraints.loose(
Size(_expanderWidthExpanded, 0.0), Size(_expanderWidthExpanded, 0.0),
) )
@ -498,8 +454,7 @@ class CardTextFieldState extends State<CardTextField> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
// Must manually add hint label because they wont show on mobile with backspace hack // Must manually add hint label because they wont show on mobile with backspace hack
if (_isMobile && if (_isMobile && _expirationController.text == '\u200b')
_expirationController.text == '\u200b')
Text('MM/YY', style: _hintTextSyle), Text('MM/YY', style: _hintTextSyle),
TextFormField( TextFormField(
key: const Key('expiration_field'), key: const Key('expiration_field'),
@ -515,37 +470,24 @@ class CardTextFieldState extends State<CardTextField> {
? _errorTextStyle ? _errorTextStyle
: _normalTextStyle, : _normalTextStyle,
validator: (content) { validator: (content) {
if (content == null || if (content == null || content.isEmpty || _isMobile && content == '\u200b') {
content.isEmpty ||
_isMobile && content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => setState(() => _cardDetails.expirationString = content.replaceAll('\u200b', ''));
_cardDetails.expirationString =
content.replaceAll('\u200b', ''));
} else { } else {
setState(() => _cardDetails setState(() => _cardDetails.expirationString = content);
.expirationString = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState == CardDetailsValidState.dateTooEarly) {
CardDetailsValidState.dateTooEarly) { _setValidationState('Your card\'s expiration date is in the past.');
_setValidationState( } else if (_cardDetails.validState == CardDetailsValidState.dateTooLate) {
'Your card\'s expiration date is in the past.'); _setValidationState('Your card\'s expiration year is invalid.');
} else if (_cardDetails.validState == } else if (_cardDetails.validState == CardDetailsValidState.missingDate) {
CardDetailsValidState.dateTooLate) { _setValidationState('You must include your card\'s expiration date.');
_setValidationState( } else if (_cardDetails.validState == CardDetailsValidState.invalidMonth) {
'Your card\'s expiration year is invalid.'); _setValidationState('Your card\'s expiration month 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; return null;
}, },
@ -554,25 +496,18 @@ class CardTextFieldState extends State<CardTextField> {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => setState(() => _cardDetails.expirationString = str.replaceAll('\u200b', ''));
_cardDetails.expirationString =
str.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails.expirationString = str);
_cardDetails.expirationString = str);
} }
if (str.length == 5) { if (str.length == 5) {
_currentCardEntryStepController _currentCardEntryStepController.add(CardEntryStep.cvc);
.add(CardEntryStep.cvc);
} }
}, },
onFieldSubmitted: (_) => onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.cvc),
_currentCardEntryStepController
.add(CardEntryStep.cvc),
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(5), LengthLimitingTextInputFormatter(5),
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(RegExp('[0-9/]')),
RegExp('[0-9/]')),
CardExpirationFormatter(), CardExpirationFormatter(),
], ],
decoration: InputDecoration( decoration: InputDecoration(
@ -591,8 +526,7 @@ class CardTextFieldState extends State<CardTextField> {
child: Stack( child: Stack(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
if (_isMobile && if (_isMobile && _securityCodeController.text == '\u200b')
_securityCodeController.text == '\u200b')
Text( Text(
'CVC', 'CVC',
style: _hintTextSyle, style: _hintTextSyle,
@ -602,67 +536,47 @@ class CardTextFieldState extends State<CardTextField> {
focusNode: securityCodeFocusNode, focusNode: securityCodeFocusNode,
controller: _securityCodeController, controller: _securityCodeController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
style: _isRedText([ style:
CardDetailsValidState.invalidCVC, _isRedText([CardDetailsValidState.invalidCVC, CardDetailsValidState.missingCVC])
CardDetailsValidState.missingCVC
])
? _errorTextStyle ? _errorTextStyle
: _normalTextStyle, : _normalTextStyle,
validator: (content) { validator: (content) {
if (content == null || if (content == null || content.isEmpty || _isMobile && content == '\u200b') {
content.isEmpty ||
_isMobile && content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => _cardDetails.securityCode = setState(() => _cardDetails.securityCode = content.replaceAll('\u200b', ''));
content.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails.securityCode = content);
_cardDetails.securityCode = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState == CardDetailsValidState.invalidCVC) {
CardDetailsValidState.invalidCVC) { _setValidationState('Your card\'s security code is invalid.');
_setValidationState( } else if (_cardDetails.validState == CardDetailsValidState.missingCVC) {
'Your card\'s security code is invalid.'); _setValidationState('Your card\'s security code is incomplete.');
} else if (_cardDetails.validState ==
CardDetailsValidState.missingCVC) {
_setValidationState(
'Your card\'s security code is incomplete.');
} }
return null; return null;
}, },
onFieldSubmitted: (_) => onFieldSubmitted: (_) => _currentCardEntryStepController.add(CardEntryStep.postal),
_currentCardEntryStepController
.add(CardEntryStep.postal),
onChanged: (str) { onChanged: (str) {
if (_isMobile) { if (_isMobile) {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => setState(() => _cardDetails.expirationString = str.replaceAll('\u200b', ''));
_cardDetails.expirationString =
str.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails.expirationString = str);
_cardDetails.expirationString = str);
} }
if (str.length == if (str.length == _cardDetails.provider?.cvcLength) {
_cardDetails.provider?.cvcLength) { _currentCardEntryStepController.add(CardEntryStep.postal);
_currentCardEntryStepController
.add(CardEntryStep.postal);
} }
}, },
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter( LengthLimitingTextInputFormatter(
_cardDetails.provider == null _cardDetails.provider == null ? 4 : _cardDetails.provider!.cvcLength),
? 4 FilteringTextInputFormatter.allow(RegExp('[0-9]')),
: _cardDetails.provider!.cvcLength),
FilteringTextInputFormatter.allow(
RegExp('[0-9]')),
], ],
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -680,8 +594,7 @@ class CardTextFieldState extends State<CardTextField> {
child: Stack( child: Stack(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
if (_isMobile && if (_isMobile && _postalCodeController.text == '\u200b')
_postalCodeController.text == '\u200b')
Text( Text(
'Postal Code', 'Postal Code',
style: _hintTextSyle, style: _hintTextSyle,
@ -691,35 +604,25 @@ class CardTextFieldState extends State<CardTextField> {
focusNode: postalCodeFocusNode, focusNode: postalCodeFocusNode,
controller: _postalCodeController, controller: _postalCodeController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
style: _isRedText([ style:
CardDetailsValidState.invalidZip, _isRedText([CardDetailsValidState.invalidZip, CardDetailsValidState.missingZip])
CardDetailsValidState.missingZip
])
? _errorTextStyle ? _errorTextStyle
: _normalTextStyle, : _normalTextStyle,
validator: (content) { validator: (content) {
if (content == null || if (content == null || content.isEmpty || _isMobile && content == '\u200b') {
content.isEmpty ||
_isMobile && content == '\u200b') {
return null; return null;
} }
if (_isMobile) { if (_isMobile) {
setState(() => _cardDetails.postalCode = setState(() => _cardDetails.postalCode = content.replaceAll('\u200b', ''));
content.replaceAll('\u200b', ''));
} else { } else {
setState(() => setState(() => _cardDetails.postalCode = content);
_cardDetails.postalCode = content);
} }
if (_cardDetails.validState == if (_cardDetails.validState == CardDetailsValidState.invalidZip) {
CardDetailsValidState.invalidZip) { _setValidationState('The postal code you entered is not correct.');
_setValidationState( } else if (_cardDetails.validState == CardDetailsValidState.missingZip) {
'The postal code you entered is not correct.'); _setValidationState('You must enter your card\'s postal code.');
} else if (_cardDetails.validState ==
CardDetailsValidState.missingZip) {
_setValidationState(
'You must enter your card\'s postal code.');
} }
return null; return null;
}, },
@ -728,11 +631,9 @@ class CardTextFieldState extends State<CardTextField> {
if (str.isEmpty) { if (str.isEmpty) {
_backspacePressed(); _backspacePressed();
} }
setState(() => _cardDetails.postalCode = setState(() => _cardDetails.postalCode = str.replaceAll('\u200b', ''));
str.replaceAll('\u200b', ''));
} else { } else {
setState( setState(() => _cardDetails.postalCode = str);
() => _cardDetails.postalCode = str);
} }
}, },
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
@ -752,12 +653,8 @@ class CardTextFieldState extends State<CardTextField> {
), ),
AnimatedOpacity( AnimatedOpacity(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
opacity: opacity: _loading && widget.showInternalLoadingWidget ? 1.0 : 0.0,
_loading && widget.showInternalLoadingWidget child: widget.loadingWidget ?? const CircularProgressIndicator(),
? 1.0
: 0.0,
child: widget.loadingWidget ??
const CircularProgressIndicator(),
), ),
], ],
), ),
@ -865,20 +762,14 @@ class CardTextFieldState extends State<CardTextField> {
_horizontalScrollController.animateTo(0.0, duration: dur, curve: cur); _horizontalScrollController.animateTo(0.0, duration: dur, curve: cur);
break; break;
case CardEntryStep.exp: case CardEntryStep.exp:
_horizontalScrollController.animateTo(_cardFieldWidth / 2, _horizontalScrollController.animateTo(_cardFieldWidth / 2, duration: dur, curve: cur);
duration: dur, curve: cur);
break; break;
case CardEntryStep.cvc: case CardEntryStep.cvc:
_horizontalScrollController.animateTo( _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth, duration: dur, curve: cur);
_cardFieldWidth / 2 + _expirationFieldWidth,
duration: dur,
curve: cur);
break; break;
case CardEntryStep.postal: case CardEntryStep.postal:
_horizontalScrollController.animateTo( _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth,
_cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth, duration: dur, curve: cur);
duration: dur,
curve: cur);
break; break;
} }
} }
@ -951,11 +842,11 @@ class CardTextFieldState extends State<CardTextField> {
case CardEntryStep.number: case CardEntryStep.number:
break; break;
case CardEntryStep.exp: case CardEntryStep.exp:
if (_expirationController.text.isNotEmpty) return; if (_expirationController.text.isNotEmpty) break;
case CardEntryStep.cvc: case CardEntryStep.cvc:
if (_securityCodeController.text.isNotEmpty) return; if (_securityCodeController.text.isNotEmpty) break;
case CardEntryStep.postal: case CardEntryStep.postal:
if (_postalCodeController.text.isNotEmpty) return; if (_postalCodeController.text.isNotEmpty) break;
} }
_transitionStepFocus(); _transitionStepFocus();
} }
@ -1002,8 +893,7 @@ class CardTextFieldState extends State<CardTextField> {
/// to make the card number display cleanly. /// to make the card number display cleanly.
class CardNumberInputFormatter implements TextInputFormatter { class CardNumberInputFormatter implements TextInputFormatter {
@override @override
TextEditingValue formatEditUpdate( TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
TextEditingValue oldValue, TextEditingValue newValue) {
String cardNum = newValue.text; String cardNum = newValue.text;
if (cardNum.length <= 4) return newValue; if (cardNum.length <= 4) return newValue;
@ -1018,9 +908,7 @@ class CardNumberInputFormatter implements TextInputFormatter {
} }
} }
return newValue.copyWith( return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length));
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length));
} }
} }
@ -1028,8 +916,7 @@ class CardNumberInputFormatter implements TextInputFormatter {
/// the month and the year for the expiration date. /// the month and the year for the expiration date.
class CardExpirationFormatter implements TextInputFormatter { class CardExpirationFormatter implements TextInputFormatter {
@override @override
TextEditingValue formatEditUpdate( TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
TextEditingValue oldValue, TextEditingValue newValue) {
String cardExp = newValue.text; String cardExp = newValue.text;
if (cardExp.length == 1) { if (cardExp.length == 1) {
if (cardExp[0] == '0' || cardExp[0] == '1') { if (cardExp[0] == '0' || cardExp[0] == '1') {
@ -1050,8 +937,6 @@ class CardExpirationFormatter implements TextInputFormatter {
buffer.write('/'); buffer.write('/');
} }
} }
return newValue.copyWith( return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length));
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length));
} }
} }

View File

@ -24,8 +24,7 @@ void main() {
final input = TestTextInput(); final input = TestTextInput();
final cardState = final cardState = tester.state(find.byType(CardTextField)) as CardTextFieldState;
tester.state(find.byType(CardTextField)) as CardTextFieldState;
assertEmptyTextFields(tester, cardState.isWideFormat); assertEmptyTextFields(tester, cardState.isWideFormat);
@ -48,8 +47,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(getTextFormField(expirationFieldKey).controller?.text, ''); expect(getTextFormField(expirationFieldKey).controller?.text, '');
expect(getTextFormField(cardFieldKey).controller?.text, expect(getTextFormField(cardFieldKey).controller?.text, '4242 4242 4242 424');
'4242 4242 4242 424');
expect(cardState.cardNumberFocusNode.hasFocus, true); expect(cardState.cardNumberFocusNode.hasFocus, true);
expect(cardState.expirationFocusNode.hasFocus, false); expect(cardState.expirationFocusNode.hasFocus, false);
// Postal code should now be gone // Postal code should now be gone
@ -60,8 +58,7 @@ void main() {
input.enterText("4242424242424242"); input.enterText("4242424242424242");
await tester.pump(); await tester.pump();
expect(getTextFormField(cardFieldKey).controller?.text, expect(getTextFormField(cardFieldKey).controller?.text, '4242 4242 4242 4242');
'4242 4242 4242 4242');
expect(cardState.cardNumberFocusNode.hasFocus, false); expect(cardState.cardNumberFocusNode.hasFocus, false);
expect(cardState.expirationFocusNode.hasFocus, true); expect(cardState.expirationFocusNode.hasFocus, true);
// Postal code should move back into view // Postal code should move back into view
@ -93,10 +90,7 @@ void main() {
await tester.pump(); await tester.pump();
final expectedCardDetails = CardDetails( final expectedCardDetails = CardDetails(
cardNumber: '4242 4242 4242 4242', cardNumber: '4242 4242 4242 4242', securityCode: '333', expirationString: '10/28', postalCode: '91555');
securityCode: '333',
expirationString: '10/28',
postalCode: '91555');
// print('${expectedCardDetails.toString()}\n${details?.toString()}'); // print('${expectedCardDetails.toString()}\n${details?.toString()}');
expect(details?.hash, expectedCardDetails.hash); expect(details?.hash, expectedCardDetails.hash);
}, },
@ -116,8 +110,7 @@ void main() {
final input = TestTextInput(); final input = TestTextInput();
final cardState = final cardState = tester.state(find.byType(CardTextField)) as CardTextFieldState;
tester.state(find.byType(CardTextField)) as CardTextFieldState;
assertEmptyTextFields(tester, cardState.isWideFormat); assertEmptyTextFields(tester, cardState.isWideFormat);
@ -142,8 +135,7 @@ void main() {
input.enterText('0055'); input.enterText('0055');
await tester.pump(); await tester.pump();
expect( expect(find.text("Your card's expiration month is invalid."), findsOneWidget);
find.text("Your card's expiration month is invalid."), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace); await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace);
await tester.pump(); await tester.pump();
@ -153,8 +145,7 @@ void main() {
input.enterText('1099'); input.enterText('1099');
await tester.pump(); await tester.pump();
expect( expect(find.text("Your card's expiration year is invalid."), findsOneWidget);
find.text("Your card's expiration year is invalid."), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace); await tester.sendKeyDownEvent(LogicalKeyboardKey.backspace);
await tester.pump(); await tester.pump();
@ -178,8 +169,7 @@ void main() {
await input.receiveAction(TextInputAction.done); await input.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
expect(find.text("The postal code you entered is not correct."), expect(find.text("The postal code you entered is not correct."), findsOneWidget);
findsOneWidget);
await tester.tap(find.byType(CardTextField)); await tester.tap(find.byType(CardTextField));
@ -192,10 +182,7 @@ void main() {
await tester.pump(); await tester.pump();
final expectedCardDetails = CardDetails( final expectedCardDetails = CardDetails(
cardNumber: '4242 4242 4242 4242', cardNumber: '4242 4242 4242 4242', expirationString: '02/28', securityCode: '123', postalCode: '12345');
expirationString: '02/28',
securityCode: '123',
postalCode: '12345');
expect(details?.hash, expectedCardDetails.hash); expect(details?.hash, expectedCardDetails.hash);
}); });
@ -222,8 +209,7 @@ void assertEmptyTextFields(WidgetTester tester, bool isWideFormat) {
// expect(find.text("Postal Code"), findsNothing); // expect(find.text("Postal Code"), findsNothing);
} }
Future<void> enterTextByKey(WidgetTester tester, Future<void> enterTextByKey(WidgetTester tester, {required String key, required String text}) async {
{required String key, required String text}) async {
await tester.enterText(find.byKey(ValueKey(key)), text); await tester.enterText(find.byKey(ValueKey(key)), text);
} }