Compare commits
No commits in common. "713f35c692eb0d81663e22548d0747534714211a" and "462e40308fd823b8cda88b5ec7e80066f4c5e6ac" have entirely different histories.
713f35c692
...
462e40308f
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,18 +1,3 @@
|
||||||
## 0.0.9
|
|
||||||
|
|
||||||
- Drastically improved usability and performance with flutter web and canvaskit renderer, especially on mobile
|
|
||||||
- Using streams to more accurately call `widget.onValidCardDetails` when the card details are valid and completed
|
|
||||||
- Added `cursorColor` customization
|
|
||||||
- Reworked widget life cycle so that hot reloads work as expected (resizing, focus, etc.).
|
|
||||||
|
|
||||||
## 0.0.8
|
|
||||||
|
|
||||||
- Updated dart sdk constraints again... oops (>=3.0.0)
|
|
||||||
|
|
||||||
## 0.0.7
|
|
||||||
|
|
||||||
- Changed pubspec versioning to allow lower SDK constraints (>=2.12.0)
|
|
||||||
|
|
||||||
## 0.0.6
|
## 0.0.6
|
||||||
|
|
||||||
- Improved assertion and error messaging when missing stripe implements
|
- Improved assertion and error messaging when missing stripe implements
|
||||||
|
|
|
@ -45,10 +45,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -135,10 +135,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.9.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -180,18 +180,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.11.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -206,7 +206,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.9"
|
version: "0.0.5"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -219,10 +219,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -267,10 +267,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.1.4-beta"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -280,5 +280,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.0"
|
version: "6.3.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
dart: ">=3.1.3 <4.0.0"
|
||||||
flutter: ">=3.7.0-0"
|
flutter: ">=3.7.0-0"
|
||||||
|
|
|
@ -4,7 +4,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Class encapsulating the card's data
|
/// Class encapsulating the card's data
|
||||||
|
@ -41,7 +39,6 @@ class CardDetails {
|
||||||
CardDetailsValidState _validState = CardDetailsValidState.blank;
|
CardDetailsValidState _validState = CardDetailsValidState.blank;
|
||||||
int _lastCheckHash = 0;
|
int _lastCheckHash = 0;
|
||||||
CardProvider? provider;
|
CardProvider? provider;
|
||||||
StreamController<CardDetails> onCompleteController = StreamController();
|
|
||||||
|
|
||||||
set overrideValidState(CardDetailsValidState state) => _validState = state;
|
set overrideValidState(CardDetailsValidState state) => _validState = state;
|
||||||
|
|
||||||
|
@ -54,6 +51,7 @@ class CardDetails {
|
||||||
String get expMonth => isComplete ? expirationString!.split('/').first : '';
|
String get expMonth => isComplete ? expirationString!.split('/').first : '';
|
||||||
String get expYear => isComplete ? expirationString!.split('/').last : '';
|
String get expYear => isComplete ? expirationString!.split('/').last : '';
|
||||||
|
|
||||||
|
// TODO rename to be more clear
|
||||||
/// 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.
|
||||||
|
@ -68,14 +66,6 @@ class CardDetails {
|
||||||
return _complete;
|
return _complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detects if the card is complete, then broadcasts
|
|
||||||
/// card details to `onCompleteController`
|
|
||||||
void broadcastStatus() {
|
|
||||||
if (isComplete) {
|
|
||||||
onCompleteController.add(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum length of the INN (identifier)
|
/// The maximum length of the INN (identifier)
|
||||||
/// of a card provider.
|
/// of a card provider.
|
||||||
int get maxINNLength => 4;
|
int get maxINNLength => 4;
|
||||||
|
@ -92,12 +82,12 @@ class CardDetails {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_complete = false;
|
|
||||||
_lastCheckHash = currentHash;
|
_lastCheckHash = currentHash;
|
||||||
if (_cardNumber == null &&
|
if (_cardNumber == null &&
|
||||||
expirationString == null &&
|
expirationString == null &&
|
||||||
securityCode == null &&
|
securityCode == null &&
|
||||||
postalCode == null) {
|
postalCode == null) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.blank;
|
_validState = CardDetailsValidState.blank;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -109,52 +99,63 @@ class CardDetails {
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (!_luhnAlgorithmCheck(nums)) {
|
if (!_luhnAlgorithmCheck(nums)) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.invalidCard;
|
_validState = CardDetailsValidState.invalidCard;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_cardNumber == null || !cardNumberFilled) {
|
if (_cardNumber == null || !cardNumberFilled) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.missingCard;
|
_validState = CardDetailsValidState.missingCard;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (expirationString == null) {
|
if (expirationString == null) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.missingDate;
|
_validState = CardDetailsValidState.missingDate;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final expSplits = expirationString!.split('/');
|
final expSplits = expirationString!.split('/');
|
||||||
if (expSplits.length != 2 || expSplits.last == '') {
|
if (expSplits.length != 2 || expSplits.last == '') {
|
||||||
|
_complete = false;
|
||||||
_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;
|
||||||
_validState = CardDetailsValidState.invalidMonth;
|
_validState = CardDetailsValidState.invalidMonth;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final year = 2000 + int.parse(expSplits.last);
|
final year = 2000 + int.parse(expSplits.last);
|
||||||
final date = DateTime(year, month);
|
final date = DateTime(year, month);
|
||||||
if (date.isBefore(DateTime.now())) {
|
if (date.isBefore(DateTime.now())) {
|
||||||
|
_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;
|
||||||
_validState = CardDetailsValidState.dateTooLate;
|
_validState = CardDetailsValidState.dateTooLate;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expirationDate = date;
|
expirationDate = date;
|
||||||
if (securityCode == null) {
|
if (securityCode == null) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.missingCVC;
|
_validState = CardDetailsValidState.missingCVC;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (provider != null && securityCode!.length != provider!.cvcLength) {
|
if (provider != null && securityCode!.length != provider!.cvcLength) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.invalidCVC;
|
_validState = CardDetailsValidState.invalidCVC;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (postalCode == null) {
|
if (postalCode == null) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.missingZip;
|
_validState = CardDetailsValidState.missingZip;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^\d{5}(-\d{4})?$').hasMatch(postalCode!)) {
|
if (!RegExp(r'^\d{5}(-\d{4})?$').hasMatch(postalCode!)) {
|
||||||
|
_complete = false;
|
||||||
_validState = CardDetailsValidState.invalidZip;
|
_validState = CardDetailsValidState.invalidZip;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,22 +42,19 @@ class CardTextField extends StatefulWidget {
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.width,
|
required this.width,
|
||||||
this.onStripeResponse,
|
this.onStripeResponse,
|
||||||
this.onCallToStripe,
|
|
||||||
this.onValidCardDetails,
|
this.onValidCardDetails,
|
||||||
this.onSubmitted,
|
|
||||||
this.stripePublishableKey,
|
this.stripePublishableKey,
|
||||||
this.height,
|
this.height,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
this.hintTextStyle,
|
this.hintTextStyle,
|
||||||
this.errorTextStyle,
|
this.errorTextStyle,
|
||||||
this.cursorColor,
|
|
||||||
this.boxDecoration,
|
this.boxDecoration,
|
||||||
this.errorBoxDecoration,
|
this.errorBoxDecoration,
|
||||||
this.loadingWidget,
|
this.loadingWidget,
|
||||||
this.loadingWidgetLocation = LoadingLocation.below,
|
this.loadingWidgetLocation = LoadingLocation.below,
|
||||||
this.autoFetchStripektoken = true,
|
|
||||||
this.showInternalLoadingWidget = true,
|
this.showInternalLoadingWidget = true,
|
||||||
this.delayToShowLoading = const Duration(milliseconds: 0),
|
this.delayToShowLoading = const Duration(milliseconds: 0),
|
||||||
|
this.onCallToStripe,
|
||||||
this.overrideValidState,
|
this.overrideValidState,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
this.cardFieldWidth,
|
this.cardFieldWidth,
|
||||||
|
@ -133,18 +130,12 @@ class CardTextField extends StatefulWidget {
|
||||||
/// If null, inherits from the `textStyle`.
|
/// If null, inherits from the `textStyle`.
|
||||||
final TextStyle? errorTextStyle;
|
final TextStyle? errorTextStyle;
|
||||||
|
|
||||||
/// Color used for the cursor, if null, inherits the primary color of the Theme
|
|
||||||
final Color? cursorColor;
|
|
||||||
|
|
||||||
/// Time to wait until showing the loading indicator when retrieving Stripe token, defaults to 0 milliseconds.
|
/// Time to wait until showing the loading indicator when retrieving Stripe token, defaults to 0 milliseconds.
|
||||||
final Duration delayToShowLoading;
|
final Duration delayToShowLoading;
|
||||||
|
|
||||||
/// Whether to show the internal loading widget on calls to Stripe
|
/// Whether to show the internal loading widget on calls to Stripe
|
||||||
final bool showInternalLoadingWidget;
|
final bool showInternalLoadingWidget;
|
||||||
|
|
||||||
/// Whether to automatically call `getStripeResponse` when the `_cardDetails` are valid.
|
|
||||||
final bool autoFetchStripektoken;
|
|
||||||
|
|
||||||
/// Stripe publishable key, starts with 'pk_'
|
/// Stripe publishable key, starts with 'pk_'
|
||||||
final String? stripePublishableKey;
|
final String? stripePublishableKey;
|
||||||
|
|
||||||
|
@ -152,15 +143,11 @@ class CardTextField extends StatefulWidget {
|
||||||
final void Function()? onCallToStripe;
|
final void Function()? onCallToStripe;
|
||||||
|
|
||||||
/// Callback that returns the stripe token for the card
|
/// Callback that returns the stripe token for the card
|
||||||
final void Function(Map<String, dynamic>?)? onStripeResponse;
|
final void Function(Map<String, dynamic>)? onStripeResponse;
|
||||||
|
|
||||||
/// Callback that returns the completed CardDetails object
|
/// Callback that returns the completed CardDetails object
|
||||||
final void Function(CardDetails)? onValidCardDetails;
|
final void Function(CardDetails)? onValidCardDetails;
|
||||||
|
|
||||||
/// Callback when the user hits enter or done in the postal code field
|
|
||||||
/// Optionally returns the `CardDetails` object if it is valid
|
|
||||||
final void Function(CardDetails?)? onSubmitted;
|
|
||||||
|
|
||||||
/// Can manually override the ValidState to surface errors returned from Stripe
|
/// Can manually override the ValidState to surface errors returned from Stripe
|
||||||
final CardDetailsValidState? overrideValidState;
|
final CardDetailsValidState? overrideValidState;
|
||||||
|
|
||||||
|
@ -172,8 +159,20 @@ class CardTextField extends StatefulWidget {
|
||||||
|
|
||||||
// CardTextFieldState? get state => _key.currentState;
|
// CardTextFieldState? get state => _key.currentState;
|
||||||
|
|
||||||
|
/// Validates the current fields and makes an http request to get the stripe
|
||||||
|
/// token for the `CardDetails` provided. Will return null if the data is not
|
||||||
|
/// complete or does not validate properly.
|
||||||
|
// Future<Map<String, dynamic>?> fetchStripeResponse() async {
|
||||||
|
// if (kDebugMode && _key.currentState == null) print('Could not fetch Stripe Response, currentState == null');
|
||||||
|
// return _key.currentState?.getStripeResponse();
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CardTextField> createState() => CardTextFieldState();
|
State<CardTextField> createState() => CardTextFieldState();
|
||||||
|
// {
|
||||||
|
// _state = CardTextFieldState();
|
||||||
|
// return _state;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State Widget for CardTextField
|
/// State Widget for CardTextField
|
||||||
|
@ -181,53 +180,51 @@ class CardTextField extends StatefulWidget {
|
||||||
/// create a GlobalKey for directly accessing
|
/// create a GlobalKey for directly accessing
|
||||||
/// the `getStripeResponse` function
|
/// the `getStripeResponse` function
|
||||||
class CardTextFieldState extends State<CardTextField> {
|
class CardTextFieldState extends State<CardTextField> {
|
||||||
late final TextEditingController _cardNumberController;
|
late TextEditingController _cardNumberController;
|
||||||
late final TextEditingController _expirationController;
|
late TextEditingController _expirationController;
|
||||||
late final TextEditingController _securityCodeController;
|
late TextEditingController _securityCodeController;
|
||||||
late final TextEditingController _postalCodeController;
|
late TextEditingController _postalCodeController;
|
||||||
final List<TextEditingController> _controllers = [];
|
|
||||||
|
|
||||||
// Not made private for access in widget tests
|
// Not made private for access in widget tests
|
||||||
late final FocusNode cardNumberFocusNode;
|
late FocusNode cardNumberFocusNode;
|
||||||
late final FocusNode expirationFocusNode;
|
late FocusNode expirationFocusNode;
|
||||||
late final FocusNode securityCodeFocusNode;
|
late FocusNode securityCodeFocusNode;
|
||||||
late final FocusNode postalCodeFocusNode;
|
late FocusNode postalCodeFocusNode;
|
||||||
|
|
||||||
// Not made private for access in widget tests
|
// Not made private for access in widget tests
|
||||||
late bool isWideFormat;
|
late final bool isWideFormat;
|
||||||
|
|
||||||
// Widget configurable styles
|
// Widget configurable styles
|
||||||
late BoxDecoration _normalBoxDecoration;
|
late final BoxDecoration _normalBoxDecoration;
|
||||||
late BoxDecoration _errorBoxDecoration;
|
late final BoxDecoration _errorBoxDecoration;
|
||||||
late TextStyle _errorTextStyle;
|
late final TextStyle _errorTextStyle;
|
||||||
late TextStyle _normalTextStyle;
|
late final TextStyle _normalTextStyle;
|
||||||
late TextStyle _hintTextSyle;
|
late final TextStyle _hintTextSyle;
|
||||||
late Color _cursorColor;
|
|
||||||
|
|
||||||
/// Width of the card number text field
|
/// Width of the card number text field
|
||||||
late double _cardFieldWidth;
|
late final double _cardFieldWidth;
|
||||||
|
|
||||||
/// Width of the expiration text field
|
/// Width of the expiration text field
|
||||||
late double _expirationFieldWidth;
|
late final double _expirationFieldWidth;
|
||||||
|
|
||||||
/// Width of the security code text field
|
/// Width of the security code text field
|
||||||
late double _securityFieldWidth;
|
late final double _securityFieldWidth;
|
||||||
|
|
||||||
/// Width of the postal code text field
|
/// Width of the postal code text field
|
||||||
late double _postalFieldWidth;
|
late final double _postalFieldWidth;
|
||||||
|
|
||||||
/// Width of the internal scrollable field, is potentially larger than the provided `widget.width`
|
/// Width of the internal scrollable field, is potentially larger than the provided `widget.width`
|
||||||
late double _internalFieldWidth;
|
late final double _internalFieldWidth;
|
||||||
|
|
||||||
/// Width of the gap between card number and expiration text fields when expanded
|
/// Width of the gap between card number and expiration text fields when expanded
|
||||||
late double _expanderWidthExpanded;
|
late final double _expanderWidthExpanded;
|
||||||
|
|
||||||
/// Width of the gap between card number and expiration text fields when collapsed
|
/// Width of the gap between card number and expiration text fields when collapsed
|
||||||
late double _expanderWidthCollapsed;
|
late final double _expanderWidthCollapsed;
|
||||||
|
|
||||||
String? _validationErrorText;
|
String? _validationErrorText;
|
||||||
bool _showBorderError = false;
|
bool _showBorderError = false;
|
||||||
late bool _isMobile;
|
final _isMobile = kIsWeb ? false : 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;
|
||||||
|
@ -242,7 +239,10 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_calculateProperties();
|
_cardFieldWidth = widget.cardFieldWidth ?? 180.0;
|
||||||
|
_expirationFieldWidth = widget.expFieldWidth ?? 70.0;
|
||||||
|
_securityFieldWidth = widget.securityFieldWidth ?? 40.0;
|
||||||
|
_postalFieldWidth = widget.postalFieldWidth ?? 95.0;
|
||||||
|
|
||||||
// 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();
|
||||||
|
@ -253,38 +253,92 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
_postalCodeController =
|
_postalCodeController =
|
||||||
TextEditingController(text: _isMobile ? '\u200b' : '');
|
TextEditingController(text: _isMobile ? '\u200b' : '');
|
||||||
|
|
||||||
_controllers.addAll([
|
// Otherwise, use `RawKeyboard` listener
|
||||||
_cardNumberController,
|
if (!_isMobile) {
|
||||||
_expirationController,
|
RawKeyboard.instance.addListener(_backspaceTransitionListener);
|
||||||
_securityCodeController,
|
}
|
||||||
_postalCodeController,
|
|
||||||
]);
|
|
||||||
|
|
||||||
cardNumberFocusNode = FocusNode();
|
cardNumberFocusNode = FocusNode();
|
||||||
expirationFocusNode = FocusNode();
|
expirationFocusNode = FocusNode();
|
||||||
securityCodeFocusNode = FocusNode();
|
securityCodeFocusNode = FocusNode();
|
||||||
postalCodeFocusNode = FocusNode();
|
postalCodeFocusNode = FocusNode();
|
||||||
|
|
||||||
// Add backspace transition listener for non mobile clients
|
_errorTextStyle =
|
||||||
if (!_isMobile) {
|
const TextStyle(color: Colors.red, fontSize: 14, inherit: true)
|
||||||
RawKeyboard.instance.addListener(_backspaceTransitionListener);
|
.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),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xffdde0e3),
|
||||||
|
width: 2.0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
).copyWith(
|
||||||
|
backgroundBlendMode: widget.boxDecoration?.backgroundBlendMode,
|
||||||
|
border: widget.boxDecoration?.border,
|
||||||
|
borderRadius: widget.boxDecoration?.borderRadius,
|
||||||
|
boxShadow: widget.boxDecoration?.boxShadow,
|
||||||
|
color: widget.boxDecoration?.color,
|
||||||
|
gradient: widget.boxDecoration?.gradient,
|
||||||
|
image: widget.boxDecoration?.image,
|
||||||
|
shape: widget.boxDecoration?.shape,
|
||||||
|
);
|
||||||
|
|
||||||
|
_errorBoxDecoration = BoxDecoration(
|
||||||
|
color: const Color(0xfff6f9fc),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.red,
|
||||||
|
width: 2.0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
).copyWith(
|
||||||
|
backgroundBlendMode: widget.errorBoxDecoration?.backgroundBlendMode,
|
||||||
|
border: widget.errorBoxDecoration?.border,
|
||||||
|
borderRadius: widget.errorBoxDecoration?.borderRadius,
|
||||||
|
boxShadow: widget.errorBoxDecoration?.boxShadow,
|
||||||
|
color: widget.errorBoxDecoration?.color,
|
||||||
|
gradient: widget.errorBoxDecoration?.gradient,
|
||||||
|
image: widget.errorBoxDecoration?.image,
|
||||||
|
shape: widget.errorBoxDecoration?.shape,
|
||||||
|
);
|
||||||
|
|
||||||
// Add listener to change focus and whatnot between fields
|
|
||||||
_currentCardEntryStepController.stream.listen(
|
_currentCardEntryStepController.stream.listen(
|
||||||
_onStepChange,
|
_onStepChange,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add listeners to know when card details are completed
|
isWideFormat = widget.width >=
|
||||||
_cardDetails.onCompleteController.stream.listen((card) async {
|
_cardFieldWidth +
|
||||||
if (widget.stripePublishableKey != null &&
|
_expirationFieldWidth +
|
||||||
widget.onStripeResponse != null &&
|
_securityFieldWidth +
|
||||||
widget.autoFetchStripektoken) {
|
_postalFieldWidth +
|
||||||
final res = await getStripeResponse();
|
60.0;
|
||||||
widget.onStripeResponse!(res);
|
if (isWideFormat) {
|
||||||
|
_internalFieldWidth = widget.width + _postalFieldWidth + 35;
|
||||||
|
_expanderWidthExpanded = widget.width -
|
||||||
|
_cardFieldWidth -
|
||||||
|
_expirationFieldWidth -
|
||||||
|
_securityFieldWidth -
|
||||||
|
35;
|
||||||
|
_expanderWidthCollapsed = widget.width -
|
||||||
|
_cardFieldWidth -
|
||||||
|
_expirationFieldWidth -
|
||||||
|
_securityFieldWidth -
|
||||||
|
_postalFieldWidth -
|
||||||
|
70;
|
||||||
|
} else {
|
||||||
|
_internalFieldWidth = _cardFieldWidth +
|
||||||
|
_expirationFieldWidth +
|
||||||
|
_securityFieldWidth +
|
||||||
|
_postalFieldWidth +
|
||||||
|
80;
|
||||||
}
|
}
|
||||||
if (widget.onValidCardDetails != null) widget.onValidCardDetails!(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -294,12 +348,10 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
_cardNumberController.dispose();
|
_cardNumberController.dispose();
|
||||||
_expirationController.dispose();
|
_expirationController.dispose();
|
||||||
_securityCodeController.dispose();
|
_securityCodeController.dispose();
|
||||||
_postalCodeController.dispose();
|
|
||||||
|
|
||||||
cardNumberFocusNode.dispose();
|
cardNumberFocusNode.dispose();
|
||||||
expirationFocusNode.dispose();
|
expirationFocusNode.dispose();
|
||||||
securityCodeFocusNode.dispose();
|
securityCodeFocusNode.dispose();
|
||||||
postalCodeFocusNode.dispose();
|
|
||||||
|
|
||||||
if (!_isMobile) {
|
if (!_isMobile) {
|
||||||
RawKeyboard.instance.removeListener(_backspaceTransitionListener);
|
RawKeyboard.instance.removeListener(_backspaceTransitionListener);
|
||||||
|
@ -310,10 +362,13 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_calculateProperties();
|
if ((widget.errorText != null || widget.overrideValidState != null) &&
|
||||||
_initStyles();
|
Object.hashAll([widget.errorText, widget.overrideValidState]) !=
|
||||||
_checkErrorOverride();
|
_prevErrorOverrideHash) {
|
||||||
|
_prevErrorOverrideHash =
|
||||||
|
Object.hashAll([widget.errorText, widget.overrideValidState]);
|
||||||
|
_validateFields();
|
||||||
|
}
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -423,8 +478,7 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
if (content == null || content.isEmpty) {
|
if (content == null || content.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// setState(() => _cardDetails.cardNumber = content);
|
_cardDetails.cardNumber = content;
|
||||||
|
|
||||||
if (_cardDetails.validState ==
|
if (_cardDetails.validState ==
|
||||||
CardDetailsValidState.invalidCard) {
|
CardDetailsValidState.invalidCard) {
|
||||||
_setValidationState(
|
_setValidationState(
|
||||||
|
@ -437,9 +491,9 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onChanged: (str) {
|
onChanged: (str) {
|
||||||
_onTextFieldChanged(
|
|
||||||
str, CardEntryStep.number);
|
|
||||||
final numbers = str.replaceAll(' ', '');
|
final numbers = str.replaceAll(' ', '');
|
||||||
|
setState(() =>
|
||||||
|
_cardDetails.cardNumber = numbers);
|
||||||
if (str.length <=
|
if (str.length <=
|
||||||
_cardDetails.maxINNLength) {
|
_cardDetails.maxINNLength) {
|
||||||
_cardDetails.detectCardProvider();
|
_cardDetails.detectCardProvider();
|
||||||
|
@ -458,7 +512,6 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
RegExp('[0-9 ]')),
|
RegExp('[0-9 ]')),
|
||||||
CardNumberInputFormatter(),
|
CardNumberInputFormatter(),
|
||||||
],
|
],
|
||||||
cursorColor: _cursorColor,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Card number',
|
hintText: 'Card number',
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
|
@ -498,7 +551,7 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
if (_isMobile &&
|
if (_isMobile &&
|
||||||
_expirationController.text ==
|
_expirationController.text ==
|
||||||
'\u200b')
|
'\u200b')
|
||||||
Text('MM/YYY', style: _hintTextSyle),
|
Text('MM/YY', style: _hintTextSyle),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
key: const Key('expiration_field'),
|
key: const Key('expiration_field'),
|
||||||
focusNode: expirationFocusNode,
|
focusNode: expirationFocusNode,
|
||||||
|
@ -520,12 +573,15 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (_isMobile) {
|
if (_isMobile) {
|
||||||
// setState(
|
setState(() =>
|
||||||
// () => _cardDetails.expirationString = content.replaceAll('\u200b', ''));
|
_cardDetails.expirationString =
|
||||||
// } else {
|
content.replaceAll(
|
||||||
// setState(() => _cardDetails.expirationString = content);
|
'\u200b', ''));
|
||||||
// }
|
} else {
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.expirationString = content);
|
||||||
|
}
|
||||||
|
|
||||||
if (_cardDetails.validState ==
|
if (_cardDetails.validState ==
|
||||||
CardDetailsValidState
|
CardDetailsValidState
|
||||||
|
@ -551,8 +607,17 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onChanged: (str) {
|
onChanged: (str) {
|
||||||
_onTextFieldChanged(
|
if (_isMobile) {
|
||||||
str, CardEntryStep.exp);
|
if (str.isEmpty) {
|
||||||
|
_backspacePressed();
|
||||||
|
}
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.expirationString =
|
||||||
|
str.replaceAll('\u200b', ''));
|
||||||
|
} else {
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.expirationString = str);
|
||||||
|
}
|
||||||
if (str.length == 5) {
|
if (str.length == 5) {
|
||||||
_currentCardEntryStepController
|
_currentCardEntryStepController
|
||||||
.add(CardEntryStep.cvc);
|
.add(CardEntryStep.cvc);
|
||||||
|
@ -567,7 +632,6 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
RegExp('[0-9/]')),
|
RegExp('[0-9/]')),
|
||||||
CardExpirationFormatter(),
|
CardExpirationFormatter(),
|
||||||
],
|
],
|
||||||
cursorColor: _cursorColor,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
hintText: _isMobile ? '' : 'MM/YY',
|
hintText: _isMobile ? '' : 'MM/YY',
|
||||||
|
@ -610,12 +674,15 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (_isMobile) {
|
if (_isMobile) {
|
||||||
// setState(
|
setState(() =>
|
||||||
// () => _cardDetails.securityCode = content.replaceAll('\u200b', ''));
|
_cardDetails.securityCode =
|
||||||
// } else {
|
content.replaceAll(
|
||||||
// setState(() => _cardDetails.securityCode = content);
|
'\u200b', ''));
|
||||||
// }
|
} else {
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.securityCode = content);
|
||||||
|
}
|
||||||
|
|
||||||
if (_cardDetails.validState ==
|
if (_cardDetails.validState ==
|
||||||
CardDetailsValidState
|
CardDetailsValidState
|
||||||
|
@ -634,8 +701,17 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
_currentCardEntryStepController
|
_currentCardEntryStepController
|
||||||
.add(CardEntryStep.postal),
|
.add(CardEntryStep.postal),
|
||||||
onChanged: (str) {
|
onChanged: (str) {
|
||||||
_onTextFieldChanged(
|
if (_isMobile) {
|
||||||
str, CardEntryStep.cvc);
|
if (str.isEmpty) {
|
||||||
|
_backspacePressed();
|
||||||
|
}
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.expirationString =
|
||||||
|
str.replaceAll('\u200b', ''));
|
||||||
|
} else {
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.expirationString = str);
|
||||||
|
}
|
||||||
|
|
||||||
if (str.length ==
|
if (str.length ==
|
||||||
_cardDetails
|
_cardDetails
|
||||||
|
@ -653,7 +729,6 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
FilteringTextInputFormatter.allow(
|
FilteringTextInputFormatter.allow(
|
||||||
RegExp('[0-9]')),
|
RegExp('[0-9]')),
|
||||||
],
|
],
|
||||||
cursorColor: _cursorColor,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
hintText: _isMobile ? '' : 'CVC',
|
hintText: _isMobile ? '' : 'CVC',
|
||||||
|
@ -696,11 +771,15 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (_isMobile) {
|
if (_isMobile) {
|
||||||
// setState(() => _cardDetails.postalCode = content.replaceAll('\u200b', ''));
|
setState(() =>
|
||||||
// } else {
|
_cardDetails.postalCode =
|
||||||
// setState(() => _cardDetails.postalCode = content);
|
content.replaceAll(
|
||||||
// }
|
'\u200b', ''));
|
||||||
|
} else {
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.postalCode = content);
|
||||||
|
}
|
||||||
|
|
||||||
if (_cardDetails.validState ==
|
if (_cardDetails.validState ==
|
||||||
CardDetailsValidState
|
CardDetailsValidState
|
||||||
|
@ -716,14 +795,22 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onChanged: (str) {
|
onChanged: (str) {
|
||||||
_onTextFieldChanged(
|
if (_isMobile) {
|
||||||
str, CardEntryStep.postal);
|
if (str.isEmpty) {
|
||||||
|
_backspacePressed();
|
||||||
|
}
|
||||||
|
setState(() => _cardDetails
|
||||||
|
.postalCode =
|
||||||
|
str.replaceAll('\u200b', ''));
|
||||||
|
} else {
|
||||||
|
setState(() =>
|
||||||
|
_cardDetails.postalCode = str);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onFieldSubmitted: (_) {
|
onFieldSubmitted: (_) {
|
||||||
_postalFieldSubmitted();
|
_postalFieldSubmitted();
|
||||||
},
|
},
|
||||||
cursorColor: _cursorColor,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
hintText:
|
hintText:
|
||||||
|
@ -775,162 +862,23 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTextFieldChanged(String str, CardEntryStep step) {
|
|
||||||
String cleanedStr;
|
|
||||||
if (_isMobile) {
|
|
||||||
cleanedStr = str.replaceAll('\u200b', '');
|
|
||||||
} else {
|
|
||||||
cleanedStr = str;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (step) {
|
|
||||||
case CardEntryStep.number:
|
|
||||||
setState(
|
|
||||||
() => _cardDetails.cardNumber = cleanedStr.replaceAll(' ', ''));
|
|
||||||
break;
|
|
||||||
case CardEntryStep.exp:
|
|
||||||
setState(() => _cardDetails.expirationString = cleanedStr);
|
|
||||||
break;
|
|
||||||
case CardEntryStep.cvc:
|
|
||||||
setState(() => _cardDetails.securityCode = cleanedStr);
|
|
||||||
break;
|
|
||||||
case CardEntryStep.postal:
|
|
||||||
setState(() => _cardDetails.postalCode = cleanedStr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isMobile && str.isEmpty) {
|
|
||||||
_mobileBackspaceDetected();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if card is complete and broadcast
|
|
||||||
_cardDetails.broadcastStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called in `initState()` as well as `build()`, determines form factor and target device
|
|
||||||
void _calculateProperties() {
|
|
||||||
// TODO skip if not needing to recalc
|
|
||||||
_cardFieldWidth = widget.cardFieldWidth ?? 180.0;
|
|
||||||
_expirationFieldWidth = widget.expFieldWidth ?? 70.0;
|
|
||||||
_securityFieldWidth = widget.securityFieldWidth ?? 40.0;
|
|
||||||
_postalFieldWidth = widget.postalFieldWidth ?? 95.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;
|
|
||||||
} else {
|
|
||||||
_internalFieldWidth = _cardFieldWidth +
|
|
||||||
_expirationFieldWidth +
|
|
||||||
_securityFieldWidth +
|
|
||||||
_postalFieldWidth +
|
|
||||||
80;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isMobile = kIsWeb ? !isWideFormat : Platform.isAndroid || Platform.isIOS;
|
|
||||||
|
|
||||||
// int index = 0;
|
|
||||||
// for (final controller in _controllers) {
|
|
||||||
// if (controller.text.isNotEmpty || index == 0) continue;
|
|
||||||
// controller.text = '\u200b';
|
|
||||||
// index += 1;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called every `build()` invocation, combines passed in styles with the defaults
|
|
||||||
void _initStyles() {
|
|
||||||
_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),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xffdde0e3),
|
|
||||||
width: 2.0,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
).copyWith(
|
|
||||||
backgroundBlendMode: widget.boxDecoration?.backgroundBlendMode,
|
|
||||||
border: widget.boxDecoration?.border,
|
|
||||||
borderRadius: widget.boxDecoration?.borderRadius,
|
|
||||||
boxShadow: widget.boxDecoration?.boxShadow,
|
|
||||||
color: widget.boxDecoration?.color,
|
|
||||||
gradient: widget.boxDecoration?.gradient,
|
|
||||||
image: widget.boxDecoration?.image,
|
|
||||||
shape: widget.boxDecoration?.shape,
|
|
||||||
);
|
|
||||||
|
|
||||||
_errorBoxDecoration = BoxDecoration(
|
|
||||||
color: const Color(0xfff6f9fc),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.red,
|
|
||||||
width: 2.0,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
).copyWith(
|
|
||||||
backgroundBlendMode: widget.errorBoxDecoration?.backgroundBlendMode,
|
|
||||||
border: widget.errorBoxDecoration?.border,
|
|
||||||
borderRadius: widget.errorBoxDecoration?.borderRadius,
|
|
||||||
boxShadow: widget.errorBoxDecoration?.boxShadow,
|
|
||||||
color: widget.errorBoxDecoration?.color,
|
|
||||||
gradient: widget.errorBoxDecoration?.gradient,
|
|
||||||
image: widget.errorBoxDecoration?.image,
|
|
||||||
shape: widget.errorBoxDecoration?.shape,
|
|
||||||
);
|
|
||||||
_cursorColor = widget.cursorColor ?? Theme.of(context).primaryColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _checkErrorOverride() {
|
|
||||||
if ((widget.errorText != null || widget.overrideValidState != null) &&
|
|
||||||
Object.hashAll([widget.errorText, widget.overrideValidState]) !=
|
|
||||||
_prevErrorOverrideHash) {
|
|
||||||
_prevErrorOverrideHash =
|
|
||||||
Object.hashAll([widget.errorText, widget.overrideValidState]);
|
|
||||||
_validateFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Makes an http call to stripe API with provided card credentials and returns the result
|
// Makes an http call to stripe API with provided card credentials and returns the result
|
||||||
Future<Map<String, dynamic>?> getStripeResponse() async {
|
Future<Map<String, dynamic>?> getStripeResponse() async {
|
||||||
if (widget.stripePublishableKey == null) {
|
|
||||||
if (kDebugMode)
|
|
||||||
print(
|
|
||||||
'***ERROR tried calling `getStripeResponse()` but no stripe key provided');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_validateFields();
|
_validateFields();
|
||||||
|
|
||||||
if (!_cardDetails.isComplete) {
|
if (!_cardDetails.isComplete) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode)
|
||||||
print(
|
print(
|
||||||
'***ERROR Could not get stripe response, card details not complete: ${_cardDetails.validState}');
|
'Could not get stripe response, card details not complete: $_cardDetails');
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
||||||
|
if (widget.stripePublishableKey == null) {
|
||||||
|
if (kDebugMode)
|
||||||
|
print(
|
||||||
|
'***ERROR tried calling `getStripeToken()` but no stripe key provided');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
bool returned = false;
|
bool returned = false;
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
|
@ -960,19 +908,16 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
|
|
||||||
Future<void> _postalFieldSubmitted() async {
|
Future<void> _postalFieldSubmitted() async {
|
||||||
_validateFields();
|
_validateFields();
|
||||||
if (widget.onSubmitted != null) {
|
|
||||||
widget.onSubmitted!(_cardDetails.isComplete ? _cardDetails : null);
|
|
||||||
}
|
|
||||||
if (_cardDetails.isComplete) {
|
if (_cardDetails.isComplete) {
|
||||||
if (widget.onValidCardDetails != null) {
|
if (widget.onValidCardDetails != null) {
|
||||||
widget.onValidCardDetails!(_cardDetails);
|
widget.onValidCardDetails!(_cardDetails);
|
||||||
} else if (widget.onStripeResponse != null &&
|
} else if (widget.onStripeResponse != null) {
|
||||||
!widget.autoFetchStripektoken) {
|
|
||||||
// Callback that stripe call is being made
|
// Callback that stripe call is being made
|
||||||
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
||||||
final jsonBody = await getStripeResponse();
|
final jsonBody = await getStripeResponse();
|
||||||
|
|
||||||
widget.onStripeResponse!(jsonBody);
|
if (jsonBody != null) widget.onStripeResponse!(jsonBody);
|
||||||
|
if (_loading) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1012,13 +957,12 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
|
|
||||||
/// Used when `_isWideFormat == false`, scrolls
|
/// Used when `_isWideFormat == false`, scrolls
|
||||||
/// the `_horizontalScrollController` to a given offset
|
/// the `_horizontalScrollController` to a given offset
|
||||||
void _scrollRow(CardEntryStep step) async {
|
void _scrollRow(CardEntryStep step) {
|
||||||
await Future.delayed(const Duration(milliseconds: 25));
|
|
||||||
const dur = Duration(milliseconds: 150);
|
const dur = Duration(milliseconds: 150);
|
||||||
const cur = Curves.easeOut;
|
const cur = Curves.easeOut;
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case CardEntryStep.number:
|
case CardEntryStep.number:
|
||||||
_horizontalScrollController.animateTo(-20.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,
|
||||||
|
@ -1043,14 +987,14 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
/// StreamController. Manages validation and tracking of the current step
|
/// StreamController. Manages validation and tracking of the current step
|
||||||
/// as well as scrolling the text fields.
|
/// as well as scrolling the text fields.
|
||||||
void _onStepChange(CardEntryStep step) {
|
void _onStepChange(CardEntryStep step) {
|
||||||
// Validated fields only when progressing, not when regressing in step
|
|
||||||
if (_currentStep.index < step.index) {
|
if (_currentStep.index < step.index) {
|
||||||
_validateFields();
|
_validateFields();
|
||||||
} else if (_currentStep != step) {
|
} else if (_currentStep != step) {
|
||||||
_setValidationState(null);
|
_setValidationState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If field tapped, and has focus, dismiss focus
|
// If field tapped, and has focus, dismiss focus
|
||||||
if (_currentStep == step && _anyHaveFocus()) {
|
if (_currentStep == step && _hasFocus()) {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1058,7 +1002,7 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentStep = step;
|
_currentStep = step;
|
||||||
});
|
});
|
||||||
switch (_currentStep) {
|
switch (step) {
|
||||||
case CardEntryStep.number:
|
case CardEntryStep.number:
|
||||||
cardNumberFocusNode.requestFocus();
|
cardNumberFocusNode.requestFocus();
|
||||||
break;
|
break;
|
||||||
|
@ -1072,54 +1016,28 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
postalCodeFocusNode.requestFocus();
|
postalCodeFocusNode.requestFocus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make the selection adjustment after first frame builds
|
|
||||||
if (kIsWeb)
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _adjustSelection());
|
|
||||||
|
|
||||||
if (!isWideFormat) {
|
if (!isWideFormat) {
|
||||||
_scrollRow(step);
|
_scrollRow(step);
|
||||||
}
|
}
|
||||||
|
// If mobile, and keyboard is closed, unfocus, to allow refocus
|
||||||
|
// print(MediaQuery.of(context).viewInsets.bottom);
|
||||||
|
// if (_isMobile && _hasFocus() && MediaQuery.of(context).viewInsets.bottom == 0.0) {
|
||||||
|
// cardNumberFocusNode.unfocus();
|
||||||
|
// expirationFocusNode.unfocus();
|
||||||
|
// securityCodeFocusNode.unfocus();
|
||||||
|
// postalCodeFocusNode.unfocus();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if any field in the `CardTextField` has focus.
|
/// Returns true if any field in the `CardTextField` has focus.
|
||||||
bool _anyHaveFocus() {
|
// ignore: unused_element
|
||||||
|
bool _hasFocus() {
|
||||||
return cardNumberFocusNode.hasFocus ||
|
return cardNumberFocusNode.hasFocus ||
|
||||||
expirationFocusNode.hasFocus ||
|
expirationFocusNode.hasFocus ||
|
||||||
securityCodeFocusNode.hasFocus ||
|
securityCodeFocusNode.hasFocus ||
|
||||||
postalCodeFocusNode.hasFocus;
|
postalCodeFocusNode.hasFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// On web, selection gets screwy when changing focus, workaround for placing cursor at the end of the text content only
|
|
||||||
void _adjustSelection() {
|
|
||||||
switch (_currentStep) {
|
|
||||||
case CardEntryStep.number:
|
|
||||||
final len = _cardNumberController.text.length;
|
|
||||||
final offset = len == 0 ? 1 : len;
|
|
||||||
_cardNumberController.value = _cardNumberController.value.copyWith(
|
|
||||||
selection: TextSelection(baseOffset: offset, extentOffset: offset));
|
|
||||||
break;
|
|
||||||
case CardEntryStep.exp:
|
|
||||||
final len = _expirationController.text.length;
|
|
||||||
final offset = len == 0 ? 0 : len;
|
|
||||||
_expirationController.value = _expirationController.value.copyWith(
|
|
||||||
selection: TextSelection(baseOffset: offset, extentOffset: offset));
|
|
||||||
break;
|
|
||||||
case CardEntryStep.cvc:
|
|
||||||
final len = _securityCodeController.text.length;
|
|
||||||
final offset = len == 0 ? 0 : len;
|
|
||||||
_securityCodeController.value = _securityCodeController.value.copyWith(
|
|
||||||
selection: TextSelection(baseOffset: offset, extentOffset: offset));
|
|
||||||
break;
|
|
||||||
case CardEntryStep.postal:
|
|
||||||
final len = _postalCodeController.text.length;
|
|
||||||
final offset = len == 0 ? 0 : len;
|
|
||||||
_postalCodeController.value = _postalCodeController.value.copyWith(
|
|
||||||
selection: TextSelection(baseOffset: offset, extentOffset: offset));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Function that is listening to the keyboard events.
|
/// Function that is listening to the keyboard events.
|
||||||
///
|
///
|
||||||
/// This provides the functionality of hitting backspace
|
/// This provides the functionality of hitting backspace
|
||||||
|
@ -1131,7 +1049,7 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
}
|
}
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case CardEntryStep.number:
|
case CardEntryStep.number:
|
||||||
return;
|
break;
|
||||||
case CardEntryStep.exp:
|
case CardEntryStep.exp:
|
||||||
if (_expirationController.text.isNotEmpty) return;
|
if (_expirationController.text.isNotEmpty) return;
|
||||||
case CardEntryStep.cvc:
|
case CardEntryStep.cvc:
|
||||||
|
@ -1142,9 +1060,8 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
_transitionStepFocus();
|
_transitionStepFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called whenever a text field is emptied and the mobile flag is set
|
void _backspacePressed() {
|
||||||
void _mobileBackspaceDetected() {
|
// Put the empty char back into the controller
|
||||||
// Put the empty char back into the controller to detect backspace on mobile
|
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case CardEntryStep.number:
|
case CardEntryStep.number:
|
||||||
break;
|
break;
|
||||||
|
@ -1164,22 +1081,18 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
break;
|
break;
|
||||||
case CardEntryStep.exp:
|
case CardEntryStep.exp:
|
||||||
_currentCardEntryStepController.add(CardEntryStep.number);
|
_currentCardEntryStepController.add(CardEntryStep.number);
|
||||||
|
String numStr = _cardNumberController.text;
|
||||||
final String numStr = _cardNumberController.text;
|
_cardNumberController.text = numStr.substring(0, numStr.length - 1);
|
||||||
final endIndex = numStr.isEmpty ? 0 : numStr.length - 1;
|
|
||||||
_cardNumberController.text = numStr.substring(0, endIndex);
|
|
||||||
break;
|
break;
|
||||||
case CardEntryStep.cvc:
|
case CardEntryStep.cvc:
|
||||||
_currentCardEntryStepController.add(CardEntryStep.exp);
|
_currentCardEntryStepController.add(CardEntryStep.exp);
|
||||||
final String expStr = _expirationController.text;
|
final expStr = _expirationController.text;
|
||||||
final endIndex = expStr.isEmpty ? 0 : expStr.length - 1;
|
_expirationController.text = expStr.substring(0, expStr.length - 1);
|
||||||
_expirationController.text = expStr.substring(0, endIndex);
|
|
||||||
break;
|
break;
|
||||||
case CardEntryStep.postal:
|
case CardEntryStep.postal:
|
||||||
_currentCardEntryStepController.add(CardEntryStep.cvc);
|
_currentCardEntryStepController.add(CardEntryStep.cvc);
|
||||||
final String cvcStr = _securityCodeController.text;
|
final String cvcStr = _securityCodeController.text;
|
||||||
final endIndex = cvcStr.isEmpty ? 0 : cvcStr.length - 1;
|
_securityCodeController.text = cvcStr.substring(0, cvcStr.length - 1);
|
||||||
_securityCodeController.text = cvcStr.substring(0, endIndex);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
name: stripe_native_card_field
|
name: stripe_native_card_field
|
||||||
description: A native flutter implementation of the elegant Stripe Card Field.
|
description: A native flutter implementation of the elegant Stripe Card Field.
|
||||||
version: 0.0.9
|
version: 0.0.6
|
||||||
repository: https://git.fosscat.com/n8r/stripe_native_card_field
|
repository: https://git.fosscat.com/n8r/stripe_native_card_field
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user