2023-11-08 15:42:09 -07:00
|
|
|
library stripe_native_card_field;
|
|
|
|
|
2023-11-14 09:58:49 -07:00
|
|
|
import 'dart:async';
|
2023-11-17 16:17:28 -07:00
|
|
|
import 'dart:convert';
|
2023-11-21 09:45:25 -07:00
|
|
|
import 'dart:io';
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
2023-11-14 09:58:49 -07:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2023-11-17 16:17:28 -07:00
|
|
|
import 'package:http/http.dart' as http;
|
2023-12-01 16:12:42 -07:00
|
|
|
import 'package:stripe_native_card_field/card_text_field_error.dart';
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
import 'card_details.dart';
|
|
|
|
import 'card_provider_icon.dart';
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Enum to track each step of the card detail
|
|
|
|
/// entry process.
|
2023-11-14 09:58:49 -07:00
|
|
|
enum CardEntryStep { number, exp, cvc, postal }
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
enum LoadingLocation { above, below }
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// A uniform text field for entering card details, based
|
|
|
|
/// on the behavior of Stripe's various html elements.
|
|
|
|
///
|
2023-11-17 16:17:28 -07:00
|
|
|
/// Required `width`.
|
2023-11-14 17:32:28 -07:00
|
|
|
///
|
2023-12-01 16:12:42 -07:00
|
|
|
/// To get the card data or stripe token, provide callbacks
|
|
|
|
/// for either `onValidCardDetails`, which will return a
|
|
|
|
/// `CardDetails` object, or `onStripeResponse`, which will
|
|
|
|
/// return a Map<String, dynamic> response from the Stripe Api.
|
|
|
|
///
|
|
|
|
/// If stripe integration is desired, you must provide both
|
|
|
|
/// `stripePublishableKey` and `onStripeResponse`, otherwise
|
|
|
|
/// `CardTextField` will `assert(false)` in debug mode or
|
|
|
|
/// throw a `CardTextFieldError` in profile or release mode
|
|
|
|
///
|
2023-11-14 17:32:28 -07:00
|
|
|
/// If the provided `width < 450.0`, the `CardTextField`
|
|
|
|
/// will scroll its content horizontally with the cursor
|
|
|
|
/// to compensate.
|
2023-11-14 09:58:49 -07:00
|
|
|
class CardTextField extends StatefulWidget {
|
2023-11-17 16:17:28 -07:00
|
|
|
CardTextField({
|
|
|
|
Key? key,
|
2023-11-21 09:45:25 -07:00
|
|
|
required this.width,
|
2023-11-17 16:17:28 -07:00
|
|
|
this.onStripeResponse,
|
2023-12-07 15:37:29 -07:00
|
|
|
this.onCallToStripe,
|
2023-12-01 16:12:42 -07:00
|
|
|
this.onValidCardDetails,
|
2023-12-07 15:37:29 -07:00
|
|
|
this.onSubmitted,
|
2023-11-17 16:17:28 -07:00
|
|
|
this.stripePublishableKey,
|
|
|
|
this.height,
|
|
|
|
this.textStyle,
|
|
|
|
this.hintTextStyle,
|
|
|
|
this.errorTextStyle,
|
2023-12-07 15:37:29 -07:00
|
|
|
this.cursorColor,
|
2023-11-17 16:17:28 -07:00
|
|
|
this.boxDecoration,
|
|
|
|
this.errorBoxDecoration,
|
|
|
|
this.loadingWidget,
|
2023-12-01 16:12:42 -07:00
|
|
|
this.loadingWidgetLocation = LoadingLocation.below,
|
2023-12-07 15:37:29 -07:00
|
|
|
this.autoFetchStripektoken = true,
|
2023-11-21 09:45:25 -07:00
|
|
|
this.showInternalLoadingWidget = true,
|
2023-12-01 16:12:42 -07:00
|
|
|
this.delayToShowLoading = const Duration(milliseconds: 0),
|
2023-11-17 16:17:28 -07:00
|
|
|
this.overrideValidState,
|
|
|
|
this.errorText,
|
2023-11-21 09:45:25 -07:00
|
|
|
this.cardFieldWidth,
|
|
|
|
this.expFieldWidth,
|
|
|
|
this.securityFieldWidth,
|
|
|
|
this.postalFieldWidth,
|
|
|
|
this.iconSize,
|
2023-11-21 13:38:02 -07:00
|
|
|
this.cardIconColor,
|
|
|
|
this.cardIconErrorColor,
|
2023-11-17 16:17:28 -07:00
|
|
|
}) : super(key: key) {
|
2023-12-01 16:12:42 -07:00
|
|
|
// Setup logic for the CardTextField
|
|
|
|
// Will assert in debug mode, otherwise will throw `CardTextFieldError` in profile or release
|
2023-11-17 16:17:28 -07:00
|
|
|
if (stripePublishableKey != null) {
|
2023-12-01 16:12:42 -07:00
|
|
|
if (!stripePublishableKey!.startsWith('pk_')) {
|
|
|
|
const msg = 'Invalid stripe key, doesn\'t start with "pk_"';
|
|
|
|
if (kDebugMode) assert(false, msg);
|
|
|
|
if (kReleaseMode || kProfileMode) {
|
2023-12-07 15:38:08 -07:00
|
|
|
throw CardTextFieldError(CardTextFieldErrorType.stripeImplementation,
|
|
|
|
details: msg);
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
2023-11-17 16:17:28 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Width of the entire CardTextField
|
2023-11-14 09:58:49 -07:00
|
|
|
final double width;
|
2023-11-14 17:34:11 -07:00
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Height of the entire CardTextField, defaults to 60.0
|
2023-11-14 09:58:49 -07:00
|
|
|
final double? height;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Width of card number field, only override if changing the default `textStyle.fontSize`, defaults to 180.0
|
|
|
|
final double? cardFieldWidth;
|
|
|
|
|
|
|
|
/// Width of expiration date field, only override if changing the default `textStyle.fontSize`, defaults to 70.0
|
|
|
|
final double? expFieldWidth;
|
|
|
|
|
|
|
|
/// Width of security number field, only override if changing the default `textStyle.fontSize`, defaults to 40.0
|
|
|
|
final double? securityFieldWidth;
|
|
|
|
|
|
|
|
/// Width of postal code field, only override if changing the default `textStyle.fontSize`, defaults to 95.0
|
|
|
|
final double? postalFieldWidth;
|
|
|
|
|
|
|
|
/// Overrides the default box decoration of the text field
|
|
|
|
final BoxDecoration? boxDecoration;
|
|
|
|
|
|
|
|
/// Overrides the default box decoration of the text field when there is a validation error
|
|
|
|
final BoxDecoration? errorBoxDecoration;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
/// Shown and overrides `LinearProgressIndicator` if the request to stripe takes longer than `delayToShowLoading`
|
|
|
|
/// Recommended to only override with a `LinearProgressIndicator` or similar widget, or spacing will be messed up
|
2023-11-17 16:17:28 -07:00
|
|
|
final Widget? loadingWidget;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Overrides default icon size of the card provider, defaults to `Size(30.0, 20.0)`
|
|
|
|
final Size? iconSize;
|
|
|
|
|
2023-11-21 13:38:02 -07:00
|
|
|
/// CSS string name of color or hex code for the card SVG icon to render
|
|
|
|
final String? cardIconColor;
|
|
|
|
|
|
|
|
/// CSS string name of color or hex code for the error card SVG icon to render
|
|
|
|
final String? cardIconErrorColor;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Determines where the loading indicator appears when contacting stripe
|
2023-12-01 16:12:42 -07:00
|
|
|
final LoadingLocation loadingWidgetLocation;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
2023-11-17 16:17:28 -07:00
|
|
|
/// Default TextStyle
|
|
|
|
final TextStyle? textStyle;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Default TextStyle for the hint text in each TextFormField.
|
|
|
|
/// If null, inherits from the `textStyle`.
|
2023-11-17 16:17:28 -07:00
|
|
|
final TextStyle? hintTextStyle;
|
|
|
|
|
|
|
|
/// TextStyle used when any TextFormField's have a validation error
|
2023-11-21 09:45:25 -07:00
|
|
|
/// If null, inherits from the `textStyle`.
|
2023-11-17 16:17:28 -07:00
|
|
|
final TextStyle? errorTextStyle;
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
/// Color used for the cursor, if null, inherits the primary color of the Theme
|
|
|
|
final Color? cursorColor;
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
/// Time to wait until showing the loading indicator when retrieving Stripe token, defaults to 0 milliseconds.
|
2023-11-17 16:17:28 -07:00
|
|
|
final Duration delayToShowLoading;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Whether to show the internal loading widget on calls to Stripe
|
|
|
|
final bool showInternalLoadingWidget;
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
/// Whether to automatically call `getStripeResponse` when the `_cardDetails` are valid.
|
|
|
|
final bool autoFetchStripektoken;
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Stripe publishable key, starts with 'pk_'
|
|
|
|
final String? stripePublishableKey;
|
|
|
|
|
|
|
|
/// Callback when the http request is made to Stripe
|
|
|
|
final void Function()? onCallToStripe;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
/// Callback that returns the stripe token for the card
|
2023-12-07 15:37:29 -07:00
|
|
|
final void Function(Map<String, dynamic>?)? onStripeResponse;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
/// Callback that returns the completed CardDetails object
|
2023-12-01 16:12:42 -07:00
|
|
|
final void Function(CardDetails)? onValidCardDetails;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
/// 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;
|
|
|
|
|
2023-11-17 16:17:28 -07:00
|
|
|
/// Can manually override the ValidState to surface errors returned from Stripe
|
2023-11-21 09:45:25 -07:00
|
|
|
final CardDetailsValidState? overrideValidState;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
/// Can manually override the errorText displayed to surface errors returned from Stripe
|
|
|
|
final String? errorText;
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
/// GlobalKey used for calling `getStripeToken` in the `CardTextFieldState`
|
|
|
|
// final GlobalKey<CardTextFieldState> _key = GlobalKey<CardTextFieldState>();
|
|
|
|
|
|
|
|
// CardTextFieldState? get state => _key.currentState;
|
|
|
|
|
2023-11-14 09:58:49 -07:00
|
|
|
@override
|
2023-11-14 15:09:03 -07:00
|
|
|
State<CardTextField> createState() => CardTextFieldState();
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// State Widget for CardTextField
|
2023-12-01 16:12:42 -07:00
|
|
|
/// Should not be used directly, except to
|
|
|
|
/// create a GlobalKey for directly accessing
|
|
|
|
/// the `getStripeResponse` function
|
2023-11-14 15:09:03 -07:00
|
|
|
class CardTextFieldState extends State<CardTextField> {
|
2023-12-07 15:37:29 -07:00
|
|
|
late final TextEditingController _cardNumberController;
|
|
|
|
late final TextEditingController _expirationController;
|
|
|
|
late final TextEditingController _securityCodeController;
|
|
|
|
late final TextEditingController _postalCodeController;
|
|
|
|
final List<TextEditingController> _controllers = [];
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-17 16:17:28 -07:00
|
|
|
// Not made private for access in widget tests
|
2023-12-07 15:37:29 -07:00
|
|
|
late final FocusNode cardNumberFocusNode;
|
|
|
|
late final FocusNode expirationFocusNode;
|
|
|
|
late final FocusNode securityCodeFocusNode;
|
|
|
|
late final FocusNode postalCodeFocusNode;
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-17 16:17:28 -07:00
|
|
|
// Not made private for access in widget tests
|
2023-12-07 15:37:29 -07:00
|
|
|
late bool isWideFormat;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
|
|
|
// Widget configurable styles
|
2023-12-07 15:37:29 -07:00
|
|
|
late BoxDecoration _normalBoxDecoration;
|
|
|
|
late BoxDecoration _errorBoxDecoration;
|
|
|
|
late TextStyle _errorTextStyle;
|
|
|
|
late TextStyle _normalTextStyle;
|
|
|
|
late TextStyle _hintTextSyle;
|
|
|
|
late Color _cursorColor;
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
/// Width of the card number text field
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _cardFieldWidth;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the expiration text field
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _expirationFieldWidth;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the security code text field
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _securityFieldWidth;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the postal code text field
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _postalFieldWidth;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the internal scrollable field, is potentially larger than the provided `widget.width`
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _internalFieldWidth;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the gap between card number and expiration text fields when expanded
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _expanderWidthExpanded;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// Width of the gap between card number and expiration text fields when collapsed
|
2023-12-07 15:37:29 -07:00
|
|
|
late double _expanderWidthCollapsed;
|
2023-11-14 09:58:49 -07:00
|
|
|
|
|
|
|
String? _validationErrorText;
|
2023-11-17 16:17:28 -07:00
|
|
|
bool _showBorderError = false;
|
2023-12-07 15:37:29 -07:00
|
|
|
late bool _isMobile;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
/// If a request to Stripe is being made
|
2023-11-17 16:17:28 -07:00
|
|
|
bool _loading = false;
|
|
|
|
final CardDetails _cardDetails = CardDetails.blank();
|
|
|
|
int _prevErrorOverrideHash = 0;
|
2023-11-14 09:58:49 -07:00
|
|
|
|
|
|
|
final _currentCardEntryStepController = StreamController<CardEntryStep>();
|
|
|
|
final _horizontalScrollController = ScrollController();
|
|
|
|
CardEntryStep _currentStep = CardEntryStep.number;
|
|
|
|
|
|
|
|
final _formFieldKey = GlobalKey<FormState>();
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2023-12-07 15:37:29 -07:00
|
|
|
_calculateProperties();
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
// No way to get backspace events on soft keyboards, so add invisible character to detect delete
|
2023-11-14 09:58:49 -07:00
|
|
|
_cardNumberController = TextEditingController();
|
2023-12-07 15:38:08 -07:00
|
|
|
_expirationController =
|
|
|
|
TextEditingController(text: _isMobile ? '\u200b' : '');
|
|
|
|
_securityCodeController =
|
|
|
|
TextEditingController(text: _isMobile ? '\u200b' : '');
|
|
|
|
_postalCodeController =
|
|
|
|
TextEditingController(text: _isMobile ? '\u200b' : '');
|
2023-12-07 15:37:29 -07:00
|
|
|
|
|
|
|
_controllers.addAll([
|
|
|
|
_cardNumberController,
|
|
|
|
_expirationController,
|
|
|
|
_securityCodeController,
|
|
|
|
_postalCodeController,
|
|
|
|
]);
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-14 15:09:03 -07:00
|
|
|
cardNumberFocusNode = FocusNode();
|
|
|
|
expirationFocusNode = FocusNode();
|
|
|
|
securityCodeFocusNode = FocusNode();
|
|
|
|
postalCodeFocusNode = FocusNode();
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// Add backspace transition listener for non mobile clients
|
|
|
|
if (!_isMobile) {
|
|
|
|
RawKeyboard.instance.addListener(_backspaceTransitionListener);
|
|
|
|
}
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// Add listener to change focus and whatnot between fields
|
2023-11-14 09:58:49 -07:00
|
|
|
_currentCardEntryStepController.stream.listen(
|
|
|
|
_onStepChange,
|
|
|
|
);
|
2023-11-21 09:45:25 -07:00
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// Add listeners to know when card details are completed
|
|
|
|
_cardDetails.onCompleteController.stream.listen((card) async {
|
2023-12-07 15:38:08 -07:00
|
|
|
if (widget.stripePublishableKey != null &&
|
|
|
|
widget.onStripeResponse != null &&
|
|
|
|
widget.autoFetchStripektoken) {
|
2023-12-07 15:37:29 -07:00
|
|
|
final res = await getStripeResponse();
|
|
|
|
widget.onStripeResponse!(res);
|
|
|
|
}
|
|
|
|
if (widget.onValidCardDetails != null) widget.onValidCardDetails!(card);
|
|
|
|
});
|
2023-11-17 16:17:28 -07:00
|
|
|
|
2023-11-14 09:58:49 -07:00
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_cardNumberController.dispose();
|
|
|
|
_expirationController.dispose();
|
|
|
|
_securityCodeController.dispose();
|
2023-12-07 15:37:29 -07:00
|
|
|
_postalCodeController.dispose();
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-14 15:09:03 -07:00
|
|
|
cardNumberFocusNode.dispose();
|
|
|
|
expirationFocusNode.dispose();
|
|
|
|
securityCodeFocusNode.dispose();
|
2023-12-07 15:37:29 -07:00
|
|
|
postalCodeFocusNode.dispose();
|
2023-11-14 09:58:49 -07:00
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
if (!_isMobile) {
|
|
|
|
RawKeyboard.instance.removeListener(_backspaceTransitionListener);
|
|
|
|
}
|
2023-11-14 09:58:49 -07:00
|
|
|
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-12-07 15:37:29 -07:00
|
|
|
_calculateProperties();
|
|
|
|
_initStyles();
|
|
|
|
_checkErrorOverride();
|
|
|
|
|
2023-11-14 09:58:49 -07:00
|
|
|
return Column(
|
2023-11-17 16:17:28 -07:00
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2023-11-14 09:58:49 -07:00
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Form(
|
|
|
|
key: _formFieldKey,
|
|
|
|
child: GestureDetector(
|
|
|
|
onTap: () {
|
|
|
|
// Focuses to the current field
|
|
|
|
_currentCardEntryStepController.add(_currentStep);
|
|
|
|
},
|
2023-11-21 09:45:25 -07:00
|
|
|
// Enable scrolling on mobile and if its narrow (not all fields visible)
|
|
|
|
onHorizontalDragUpdate: (details) {
|
2023-11-21 11:46:42 -07:00
|
|
|
const minOffset = 0.0;
|
2023-12-07 15:38:08 -07:00
|
|
|
final maxOffset =
|
|
|
|
_horizontalScrollController.position.maxScrollExtent;
|
2023-11-21 09:45:25 -07:00
|
|
|
if (!_isMobile || isWideFormat) return;
|
2023-12-07 15:38:08 -07:00
|
|
|
final newOffset =
|
|
|
|
_horizontalScrollController.offset - details.delta.dx;
|
2023-11-21 09:45:25 -07:00
|
|
|
|
2023-11-21 11:46:42 -07:00
|
|
|
if (newOffset < minOffset) {
|
|
|
|
_horizontalScrollController.jumpTo(minOffset);
|
|
|
|
} else if (newOffset > maxOffset) {
|
|
|
|
_horizontalScrollController.jumpTo(maxOffset);
|
2023-11-21 09:45:25 -07:00
|
|
|
} else {
|
|
|
|
_horizontalScrollController.jumpTo(newOffset);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onHorizontalDragEnd: (details) {
|
2023-12-07 15:38:08 -07:00
|
|
|
if (!_isMobile ||
|
|
|
|
isWideFormat ||
|
|
|
|
details.primaryVelocity == null) {
|
2023-11-22 10:52:52 -07:00
|
|
|
return;
|
|
|
|
}
|
2023-11-21 09:45:25 -07:00
|
|
|
|
|
|
|
const dur = Duration(milliseconds: 300);
|
|
|
|
const cur = Curves.ease;
|
|
|
|
|
|
|
|
// final max = _horizontalScrollController.position.maxScrollExtent;
|
2023-12-07 15:38:08 -07:00
|
|
|
final newOffset = _horizontalScrollController.offset -
|
|
|
|
details.primaryVelocity! * 0.15;
|
|
|
|
_horizontalScrollController.animateTo(newOffset,
|
|
|
|
curve: cur, duration: dur);
|
2023-11-21 09:45:25 -07:00
|
|
|
},
|
2023-11-14 09:58:49 -07:00
|
|
|
child: Container(
|
|
|
|
width: widget.width,
|
|
|
|
height: widget.height ?? 60.0,
|
2023-12-07 15:38:08 -07:00
|
|
|
decoration:
|
|
|
|
_showBorderError ? _errorBoxDecoration : _normalBoxDecoration,
|
2023-11-14 09:58:49 -07:00
|
|
|
child: ClipRect(
|
|
|
|
child: IgnorePointer(
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
controller: _horizontalScrollController,
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
child: SizedBox(
|
|
|
|
width: _internalFieldWidth,
|
|
|
|
height: widget.height ?? 60.0,
|
2023-12-01 16:12:42 -07:00
|
|
|
child: Column(
|
2023-11-14 09:58:49 -07:00
|
|
|
children: [
|
2023-12-07 15:38:08 -07:00
|
|
|
if (widget.loadingWidgetLocation ==
|
|
|
|
LoadingLocation.above)
|
2023-12-01 16:12:42 -07:00
|
|
|
AnimatedOpacity(
|
|
|
|
duration: const Duration(milliseconds: 300),
|
2023-12-07 15:38:08 -07:00
|
|
|
opacity:
|
|
|
|
_loading && widget.showInternalLoadingWidget
|
|
|
|
? 1.0
|
|
|
|
: 0.0,
|
|
|
|
child: widget.loadingWidget ??
|
|
|
|
const LinearProgressIndicator(),
|
2023-11-14 09:58:49 -07:00
|
|
|
),
|
2023-12-01 16:12:42 -07:00
|
|
|
Padding(
|
|
|
|
padding: switch (widget.loadingWidgetLocation) {
|
2023-12-07 15:38:08 -07:00
|
|
|
LoadingLocation.above =>
|
|
|
|
const EdgeInsets.only(top: 0, bottom: 4.0),
|
|
|
|
LoadingLocation.below =>
|
|
|
|
const EdgeInsets.only(top: 4.0, bottom: 0),
|
2023-12-01 16:12:42 -07:00
|
|
|
},
|
|
|
|
child: Row(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
2023-11-21 09:45:25 -07:00
|
|
|
children: [
|
2023-12-01 16:12:42 -07:00
|
|
|
Padding(
|
2023-12-07 15:38:08 -07:00
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
horizontal: 6.0),
|
2023-12-01 16:12:42 -07:00
|
|
|
child: CardProviderIcon(
|
|
|
|
cardDetails: _cardDetails,
|
|
|
|
size: widget.iconSize,
|
|
|
|
defaultCardColor: widget.cardIconColor,
|
|
|
|
errorCardColor: widget.cardIconErrorColor,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
width: _cardFieldWidth,
|
|
|
|
child: TextFormField(
|
|
|
|
key: const Key('card_field'),
|
|
|
|
focusNode: cardNumberFocusNode,
|
|
|
|
controller: _cardNumberController,
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
style: _isRedText([
|
|
|
|
CardDetailsValidState.invalidCard,
|
|
|
|
CardDetailsValidState.missingCard,
|
|
|
|
CardDetailsValidState.blank
|
|
|
|
])
|
|
|
|
? _errorTextStyle
|
|
|
|
: _normalTextStyle,
|
|
|
|
validator: (content) {
|
|
|
|
if (content == null || content.isEmpty) {
|
|
|
|
return null;
|
|
|
|
}
|
2023-12-07 15:37:29 -07:00
|
|
|
// setState(() => _cardDetails.cardNumber = content);
|
|
|
|
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState.invalidCard) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card number is invalid.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState.missingCard) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card number is incomplete.');
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
2023-11-21 09:45:25 -07:00
|
|
|
return null;
|
2023-12-01 16:12:42 -07:00
|
|
|
},
|
|
|
|
onChanged: (str) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_onTextFieldChanged(
|
|
|
|
str, CardEntryStep.number);
|
2023-12-01 16:12:42 -07:00
|
|
|
final numbers = str.replaceAll(' ', '');
|
2023-12-07 15:38:08 -07:00
|
|
|
if (str.length <=
|
|
|
|
_cardDetails.maxINNLength) {
|
2023-12-01 16:12:42 -07:00
|
|
|
_cardDetails.detectCardProvider();
|
2023-11-21 09:45:25 -07:00
|
|
|
}
|
2023-12-01 16:12:42 -07:00
|
|
|
if (numbers.length == 16) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.exp);
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
},
|
2023-12-07 15:38:08 -07:00
|
|
|
onFieldSubmitted: (_) =>
|
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.exp),
|
2023-12-01 16:12:42 -07:00
|
|
|
inputFormatters: [
|
|
|
|
LengthLimitingTextInputFormatter(19),
|
2023-12-07 15:38:08 -07:00
|
|
|
FilteringTextInputFormatter.allow(
|
|
|
|
RegExp('[0-9 ]')),
|
2023-12-01 16:12:42 -07:00
|
|
|
CardNumberInputFormatter(),
|
|
|
|
],
|
2023-12-07 15:37:29 -07:00
|
|
|
cursorColor: _cursorColor,
|
2023-12-01 16:12:42 -07:00
|
|
|
decoration: InputDecoration(
|
|
|
|
hintText: 'Card number',
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
hintStyle: _hintTextSyle,
|
|
|
|
fillColor: Colors.transparent,
|
|
|
|
border: InputBorder.none,
|
|
|
|
),
|
2023-11-21 09:45:25 -07:00
|
|
|
),
|
|
|
|
),
|
2023-12-01 16:12:42 -07:00
|
|
|
if (isWideFormat)
|
|
|
|
Flexible(
|
|
|
|
fit: FlexFit.loose,
|
|
|
|
// fit: _currentStep == CardEntryStep.number ? FlexFit.loose : FlexFit.tight,
|
|
|
|
child: AnimatedContainer(
|
|
|
|
curve: Curves.easeInOut,
|
2023-12-07 15:38:08 -07:00
|
|
|
duration:
|
|
|
|
const Duration(milliseconds: 400),
|
|
|
|
constraints: _currentStep ==
|
|
|
|
CardEntryStep.number
|
2023-12-01 16:12:42 -07:00
|
|
|
? BoxConstraints.loose(
|
|
|
|
Size(_expanderWidthExpanded, 0.0),
|
|
|
|
)
|
|
|
|
: BoxConstraints.tight(
|
2023-12-07 15:38:08 -07:00
|
|
|
Size(
|
|
|
|
_expanderWidthCollapsed, 0.0),
|
2023-12-01 16:12:42 -07:00
|
|
|
),
|
|
|
|
),
|
2023-11-21 09:45:25 -07:00
|
|
|
),
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
// Spacer(flex: _currentStep == CardEntryStep.number ? 100 : 1),
|
|
|
|
SizedBox(
|
|
|
|
width: _expirationFieldWidth,
|
|
|
|
child: Stack(
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
children: [
|
|
|
|
// Must manually add hint label because they wont show on mobile with backspace hack
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_isMobile &&
|
|
|
|
_expirationController.text ==
|
|
|
|
'\u200b')
|
2023-12-07 16:03:02 -07:00
|
|
|
Text('MM/YY', style: _hintTextSyle),
|
2023-12-01 16:12:42 -07:00
|
|
|
TextFormField(
|
|
|
|
key: const Key('expiration_field'),
|
|
|
|
focusNode: expirationFocusNode,
|
|
|
|
controller: _expirationController,
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
style: _isRedText([
|
|
|
|
CardDetailsValidState.dateTooLate,
|
|
|
|
CardDetailsValidState.dateTooEarly,
|
|
|
|
CardDetailsValidState.missingDate,
|
|
|
|
CardDetailsValidState.invalidMonth
|
|
|
|
])
|
|
|
|
? _errorTextStyle
|
|
|
|
: _normalTextStyle,
|
|
|
|
validator: (content) {
|
2023-12-07 15:38:08 -07:00
|
|
|
if (content == null ||
|
|
|
|
content.isEmpty ||
|
|
|
|
_isMobile &&
|
|
|
|
content == '\u200b') {
|
2023-12-01 16:12:42 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// if (_isMobile) {
|
|
|
|
// setState(
|
|
|
|
// () => _cardDetails.expirationString = content.replaceAll('\u200b', ''));
|
|
|
|
// } else {
|
|
|
|
// setState(() => _cardDetails.expirationString = content);
|
|
|
|
// }
|
|
|
|
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.dateTooEarly) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card\'s expiration date is in the past.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.dateTooLate) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card\'s expiration year is invalid.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.missingDate) {
|
|
|
|
_setValidationState(
|
|
|
|
'You must include your card\'s expiration date.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.invalidMonth) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card\'s expiration month is invalid.');
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
onChanged: (str) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_onTextFieldChanged(
|
|
|
|
str, CardEntryStep.exp);
|
2023-12-01 16:12:42 -07:00
|
|
|
if (str.length == 5) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.cvc);
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
},
|
2023-12-07 15:38:08 -07:00
|
|
|
onFieldSubmitted: (_) =>
|
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.cvc),
|
2023-12-01 16:12:42 -07:00
|
|
|
inputFormatters: [
|
|
|
|
LengthLimitingTextInputFormatter(5),
|
2023-12-07 15:38:08 -07:00
|
|
|
FilteringTextInputFormatter.allow(
|
|
|
|
RegExp('[0-9/]')),
|
2023-12-01 16:12:42 -07:00
|
|
|
CardExpirationFormatter(),
|
|
|
|
],
|
2023-12-07 15:37:29 -07:00
|
|
|
cursorColor: _cursorColor,
|
2023-12-01 16:12:42 -07:00
|
|
|
decoration: InputDecoration(
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
hintText: _isMobile ? '' : 'MM/YY',
|
|
|
|
hintStyle: _hintTextSyle,
|
|
|
|
fillColor: Colors.transparent,
|
|
|
|
border: InputBorder.none,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2023-11-21 09:45:25 -07:00
|
|
|
),
|
|
|
|
),
|
2023-12-01 16:12:42 -07:00
|
|
|
SizedBox(
|
|
|
|
width: _securityFieldWidth,
|
|
|
|
child: Stack(
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
children: [
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_isMobile &&
|
|
|
|
_securityCodeController.text ==
|
|
|
|
'\u200b')
|
2023-12-01 16:12:42 -07:00
|
|
|
Text(
|
|
|
|
'CVC',
|
|
|
|
style: _hintTextSyle,
|
|
|
|
),
|
|
|
|
TextFormField(
|
|
|
|
key: const Key('security_field'),
|
|
|
|
focusNode: securityCodeFocusNode,
|
|
|
|
controller: _securityCodeController,
|
|
|
|
keyboardType: TextInputType.number,
|
2023-12-07 15:38:08 -07:00
|
|
|
style: _isRedText([
|
|
|
|
CardDetailsValidState.invalidCVC,
|
|
|
|
CardDetailsValidState.missingCVC
|
|
|
|
])
|
2023-12-01 16:12:42 -07:00
|
|
|
? _errorTextStyle
|
|
|
|
: _normalTextStyle,
|
|
|
|
validator: (content) {
|
2023-12-07 15:38:08 -07:00
|
|
|
if (content == null ||
|
|
|
|
content.isEmpty ||
|
|
|
|
_isMobile &&
|
|
|
|
content == '\u200b') {
|
2023-12-01 16:12:42 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// if (_isMobile) {
|
|
|
|
// setState(
|
|
|
|
// () => _cardDetails.securityCode = content.replaceAll('\u200b', ''));
|
|
|
|
// } else {
|
|
|
|
// setState(() => _cardDetails.securityCode = content);
|
|
|
|
// }
|
|
|
|
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.invalidCVC) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card\'s security code is invalid.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.missingCVC) {
|
|
|
|
_setValidationState(
|
|
|
|
'Your card\'s security code is incomplete.');
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
onFieldSubmitted: (_) =>
|
2023-12-07 15:38:08 -07:00
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.postal),
|
2023-12-01 16:12:42 -07:00
|
|
|
onChanged: (str) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_onTextFieldChanged(
|
|
|
|
str, CardEntryStep.cvc);
|
|
|
|
|
|
|
|
if (str.length ==
|
|
|
|
_cardDetails
|
|
|
|
.provider?.cvcLength) {
|
|
|
|
_currentCardEntryStepController
|
|
|
|
.add(CardEntryStep.postal);
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
inputFormatters: [
|
|
|
|
LengthLimitingTextInputFormatter(
|
2023-12-07 15:38:08 -07:00
|
|
|
_cardDetails.provider == null
|
|
|
|
? 4
|
|
|
|
: _cardDetails
|
|
|
|
.provider!.cvcLength),
|
|
|
|
FilteringTextInputFormatter.allow(
|
|
|
|
RegExp('[0-9]')),
|
2023-12-01 16:12:42 -07:00
|
|
|
],
|
2023-12-07 15:37:29 -07:00
|
|
|
cursorColor: _cursorColor,
|
2023-12-01 16:12:42 -07:00
|
|
|
decoration: InputDecoration(
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
hintText: _isMobile ? '' : 'CVC',
|
|
|
|
hintStyle: _hintTextSyle,
|
|
|
|
fillColor: Colors.transparent,
|
|
|
|
border: InputBorder.none,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2023-11-21 09:45:25 -07:00
|
|
|
),
|
2023-12-01 16:12:42 -07:00
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
width: _postalFieldWidth,
|
|
|
|
child: Stack(
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
children: [
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_isMobile &&
|
|
|
|
_postalCodeController.text ==
|
|
|
|
'\u200b')
|
2023-12-01 16:12:42 -07:00
|
|
|
Text(
|
|
|
|
'Postal Code',
|
|
|
|
style: _hintTextSyle,
|
|
|
|
),
|
|
|
|
TextFormField(
|
|
|
|
key: const Key('postal_field'),
|
|
|
|
focusNode: postalCodeFocusNode,
|
|
|
|
controller: _postalCodeController,
|
|
|
|
keyboardType: TextInputType.number,
|
2023-12-07 15:38:08 -07:00
|
|
|
style: _isRedText([
|
|
|
|
CardDetailsValidState.invalidZip,
|
|
|
|
CardDetailsValidState.missingZip
|
|
|
|
])
|
2023-12-01 16:12:42 -07:00
|
|
|
? _errorTextStyle
|
|
|
|
: _normalTextStyle,
|
|
|
|
validator: (content) {
|
2023-12-07 15:38:08 -07:00
|
|
|
if (content == null ||
|
|
|
|
content.isEmpty ||
|
|
|
|
_isMobile &&
|
|
|
|
content == '\u200b') {
|
2023-12-01 16:12:42 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
// if (_isMobile) {
|
|
|
|
// setState(() => _cardDetails.postalCode = content.replaceAll('\u200b', ''));
|
|
|
|
// } else {
|
|
|
|
// setState(() => _cardDetails.postalCode = content);
|
|
|
|
// }
|
2023-12-01 16:12:42 -07:00
|
|
|
|
2023-12-07 15:38:08 -07:00
|
|
|
if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.invalidZip) {
|
|
|
|
_setValidationState(
|
|
|
|
'The postal code you entered is not correct.');
|
|
|
|
} else if (_cardDetails.validState ==
|
|
|
|
CardDetailsValidState
|
|
|
|
.missingZip) {
|
|
|
|
_setValidationState(
|
|
|
|
'You must enter your card\'s postal code.');
|
2023-12-01 16:12:42 -07:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
onChanged: (str) {
|
2023-12-07 15:38:08 -07:00
|
|
|
_onTextFieldChanged(
|
|
|
|
str, CardEntryStep.postal);
|
2023-12-01 16:12:42 -07:00
|
|
|
},
|
|
|
|
textInputAction: TextInputAction.done,
|
|
|
|
onFieldSubmitted: (_) {
|
|
|
|
_postalFieldSubmitted();
|
|
|
|
},
|
2023-12-07 15:37:29 -07:00
|
|
|
cursorColor: _cursorColor,
|
2023-12-01 16:12:42 -07:00
|
|
|
decoration: InputDecoration(
|
|
|
|
contentPadding: EdgeInsets.zero,
|
2023-12-07 15:38:08 -07:00
|
|
|
hintText:
|
|
|
|
_isMobile ? '' : 'Postal Code',
|
2023-12-01 16:12:42 -07:00
|
|
|
hintStyle: _hintTextSyle,
|
|
|
|
fillColor: Colors.transparent,
|
|
|
|
border: InputBorder.none,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2023-11-21 09:45:25 -07:00
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2023-11-14 09:58:49 -07:00
|
|
|
),
|
|
|
|
),
|
2023-12-07 15:38:08 -07:00
|
|
|
if (widget.loadingWidgetLocation ==
|
|
|
|
LoadingLocation.below)
|
2023-12-01 16:12:42 -07:00
|
|
|
AnimatedOpacity(
|
|
|
|
duration: const Duration(milliseconds: 300),
|
2023-12-07 15:38:08 -07:00
|
|
|
opacity:
|
|
|
|
_loading && widget.showInternalLoadingWidget
|
|
|
|
? 1.0
|
|
|
|
: 0.0,
|
|
|
|
child: widget.loadingWidget ??
|
|
|
|
const LinearProgressIndicator(),
|
2023-12-01 16:12:42 -07:00
|
|
|
),
|
2023-11-14 09:58:49 -07:00
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
AnimatedOpacity(
|
|
|
|
duration: const Duration(milliseconds: 525),
|
|
|
|
opacity: _validationErrorText == null ? 0.0 : 1.0,
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.only(top: 8.0, left: 14.0),
|
|
|
|
child: Text(
|
2023-11-21 09:45:25 -07:00
|
|
|
// Spacing changes by like a pixel if its an empty string, slight jitter when error appears and disappears
|
|
|
|
_validationErrorText ?? ' ',
|
2023-11-21 13:38:02 -07:00
|
|
|
style: _errorTextStyle,
|
2023-11-14 09:58:49 -07:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
void _onTextFieldChanged(String str, CardEntryStep step) {
|
|
|
|
String cleanedStr;
|
|
|
|
if (_isMobile) {
|
|
|
|
cleanedStr = str.replaceAll('\u200b', '');
|
|
|
|
} else {
|
|
|
|
cleanedStr = str;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (step) {
|
|
|
|
case CardEntryStep.number:
|
2023-12-07 15:38:08 -07:00
|
|
|
setState(
|
|
|
|
() => _cardDetails.cardNumber = cleanedStr.replaceAll(' ', ''));
|
2023-12-07 15:37:29 -07:00
|
|
|
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;
|
2023-12-07 15:38:08 -07:00
|
|
|
isWideFormat = widget.width >=
|
|
|
|
_cardFieldWidth +
|
|
|
|
_expirationFieldWidth +
|
|
|
|
_securityFieldWidth +
|
|
|
|
_postalFieldWidth +
|
|
|
|
60.0;
|
2023-12-07 15:37:29 -07:00
|
|
|
if (isWideFormat) {
|
|
|
|
_internalFieldWidth = widget.width + _postalFieldWidth + 35;
|
2023-12-07 15:38:08 -07:00
|
|
|
_expanderWidthExpanded = widget.width -
|
|
|
|
_cardFieldWidth -
|
|
|
|
_expirationFieldWidth -
|
|
|
|
_securityFieldWidth -
|
|
|
|
35;
|
|
|
|
_expanderWidthCollapsed = widget.width -
|
|
|
|
_cardFieldWidth -
|
|
|
|
_expirationFieldWidth -
|
|
|
|
_securityFieldWidth -
|
|
|
|
_postalFieldWidth -
|
|
|
|
70;
|
2023-12-07 15:37:29 -07:00
|
|
|
} else {
|
2023-12-07 15:38:08 -07:00
|
|
|
_internalFieldWidth = _cardFieldWidth +
|
|
|
|
_expirationFieldWidth +
|
|
|
|
_securityFieldWidth +
|
|
|
|
_postalFieldWidth +
|
|
|
|
80;
|
2023-12-07 15:37:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
_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() {
|
2023-12-07 15:38:08 -07:00
|
|
|
_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);
|
2023-12-07 15:37:29 -07:00
|
|
|
|
|
|
|
_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) &&
|
2023-12-07 15:38:08 -07:00
|
|
|
Object.hashAll([widget.errorText, widget.overrideValidState]) !=
|
|
|
|
_prevErrorOverrideHash) {
|
|
|
|
_prevErrorOverrideHash =
|
|
|
|
Object.hashAll([widget.errorText, widget.overrideValidState]);
|
2023-12-07 15:37:29 -07:00
|
|
|
_validateFields();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
// Makes an http call to stripe API with provided card credentials and returns the result
|
|
|
|
Future<Map<String, dynamic>?> getStripeResponse() async {
|
2023-12-07 15:37:29 -07:00
|
|
|
if (widget.stripePublishableKey == null) {
|
2023-12-07 16:24:17 -07:00
|
|
|
if (kDebugMode) {
|
2023-12-07 15:38:08 -07:00
|
|
|
print(
|
|
|
|
'***ERROR tried calling `getStripeResponse()` but no stripe key provided');
|
2023-12-07 16:24:17 -07:00
|
|
|
}
|
2023-12-07 15:37:29 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
_validateFields();
|
|
|
|
|
|
|
|
if (!_cardDetails.isComplete) {
|
2023-12-07 15:37:29 -07:00
|
|
|
if (kDebugMode) {
|
2023-12-07 15:38:08 -07:00
|
|
|
print(
|
|
|
|
'***ERROR Could not get stripe response, card details not complete: ${_cardDetails.validState}');
|
2023-12-07 15:37:29 -07:00
|
|
|
}
|
2023-12-01 16:12:42 -07:00
|
|
|
return null;
|
|
|
|
}
|
2023-12-07 15:37:29 -07:00
|
|
|
|
2023-12-01 16:12:42 -07:00
|
|
|
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
|
|
|
|
|
|
|
bool returned = false;
|
|
|
|
Future.delayed(
|
|
|
|
widget.delayToShowLoading,
|
|
|
|
() => returned ? null : setState(() => _loading = true),
|
|
|
|
);
|
|
|
|
|
|
|
|
const stripeCardUrl = 'https://api.stripe.com/v1/tokens';
|
|
|
|
final response = await http.post(
|
|
|
|
Uri.parse(stripeCardUrl),
|
|
|
|
body: {
|
|
|
|
'card[number]': _cardDetails.cardNumber,
|
|
|
|
'card[cvc]': _cardDetails.securityCode,
|
|
|
|
'card[exp_month]': _cardDetails.expMonth,
|
|
|
|
'card[exp_year]': _cardDetails.expYear,
|
|
|
|
'card[address_zip]': _cardDetails.postalCode,
|
|
|
|
'key': widget.stripePublishableKey,
|
|
|
|
},
|
|
|
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
|
|
|
);
|
|
|
|
|
|
|
|
returned = true;
|
|
|
|
final Map<String, dynamic> jsonBody = jsonDecode(response.body);
|
|
|
|
if (_loading) setState(() => _loading = false);
|
|
|
|
return jsonBody;
|
|
|
|
}
|
|
|
|
|
2023-11-21 09:45:25 -07:00
|
|
|
Future<void> _postalFieldSubmitted() async {
|
|
|
|
_validateFields();
|
2023-12-07 15:37:29 -07:00
|
|
|
if (widget.onSubmitted != null) {
|
|
|
|
widget.onSubmitted!(_cardDetails.isComplete ? _cardDetails : null);
|
|
|
|
}
|
2023-11-21 09:45:25 -07:00
|
|
|
if (_cardDetails.isComplete) {
|
2023-12-01 16:12:42 -07:00
|
|
|
if (widget.onValidCardDetails != null) {
|
|
|
|
widget.onValidCardDetails!(_cardDetails);
|
2023-12-07 15:38:08 -07:00
|
|
|
} else if (widget.onStripeResponse != null &&
|
|
|
|
!widget.autoFetchStripektoken) {
|
2023-11-21 09:45:25 -07:00
|
|
|
// Callback that stripe call is being made
|
|
|
|
if (widget.onCallToStripe != null) widget.onCallToStripe!();
|
2023-12-01 16:12:42 -07:00
|
|
|
final jsonBody = await getStripeResponse();
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
widget.onStripeResponse!(jsonBody);
|
2023-11-21 09:45:25 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Provided a list of `ValidState`, returns whether
|
|
|
|
/// make the text field red
|
2023-11-21 09:45:25 -07:00
|
|
|
bool _isRedText(List<CardDetailsValidState> args) {
|
2023-11-14 09:58:49 -07:00
|
|
|
return _showBorderError && args.contains(_cardDetails.validState);
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Helper function to change the `_showBorderError` and
|
|
|
|
/// `_validationErrorText`.
|
2023-11-14 09:58:49 -07:00
|
|
|
void _setValidationState(String? text) {
|
|
|
|
setState(() {
|
|
|
|
_validationErrorText = text;
|
|
|
|
_showBorderError = text != null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Calls `validate()` on the form state and resets
|
|
|
|
/// the validation state
|
2023-11-14 09:58:49 -07:00
|
|
|
void _validateFields() {
|
|
|
|
_validationErrorText = null;
|
2023-11-17 16:17:28 -07:00
|
|
|
if (widget.overrideValidState != null) {
|
|
|
|
_cardDetails.overrideValidState = widget.overrideValidState!;
|
|
|
|
_setValidationState(widget.errorText);
|
|
|
|
} else {
|
|
|
|
_formFieldKey.currentState!.validate();
|
|
|
|
|
|
|
|
// Clear up validation state if everything is valid
|
|
|
|
if (_validationErrorText == null) {
|
|
|
|
_setValidationState(null);
|
|
|
|
}
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Used when `_isWideFormat == false`, scrolls
|
2023-11-14 17:34:11 -07:00
|
|
|
/// the `_horizontalScrollController` to a given offset
|
2023-12-07 15:37:29 -07:00
|
|
|
void _scrollRow(CardEntryStep step) async {
|
|
|
|
await Future.delayed(const Duration(milliseconds: 25));
|
2023-11-14 15:09:03 -07:00
|
|
|
const dur = Duration(milliseconds: 150);
|
|
|
|
const cur = Curves.easeOut;
|
2023-11-14 09:58:49 -07:00
|
|
|
switch (step) {
|
|
|
|
case CardEntryStep.number:
|
2023-12-07 15:37:29 -07:00
|
|
|
_horizontalScrollController.animateTo(-20.0, duration: dur, curve: cur);
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.exp:
|
2023-12-07 15:38:08 -07:00
|
|
|
_horizontalScrollController.animateTo(_cardFieldWidth / 2,
|
|
|
|
duration: dur, curve: cur);
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.cvc:
|
2023-12-07 15:38:08 -07:00
|
|
|
_horizontalScrollController.animateTo(
|
|
|
|
_cardFieldWidth / 2 + _expirationFieldWidth,
|
|
|
|
duration: dur,
|
|
|
|
curve: cur);
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.postal:
|
2023-12-07 15:38:08 -07:00
|
|
|
_horizontalScrollController.animateTo(
|
|
|
|
_cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth,
|
|
|
|
duration: dur,
|
|
|
|
curve: cur);
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Function that is listening to the `_currentCardEntryStepController`
|
|
|
|
/// StreamController. Manages validation and tracking of the current step
|
|
|
|
/// as well as scrolling the text fields.
|
2023-11-14 09:58:49 -07:00
|
|
|
void _onStepChange(CardEntryStep step) {
|
2023-12-07 15:37:29 -07:00
|
|
|
// Validated fields only when progressing, not when regressing in step
|
2023-11-14 09:58:49 -07:00
|
|
|
if (_currentStep.index < step.index) {
|
|
|
|
_validateFields();
|
|
|
|
} else if (_currentStep != step) {
|
|
|
|
_setValidationState(null);
|
|
|
|
}
|
2023-11-21 11:46:42 -07:00
|
|
|
// If field tapped, and has focus, dismiss focus
|
2023-12-07 15:37:29 -07:00
|
|
|
if (_currentStep == step && _anyHaveFocus()) {
|
2023-11-21 11:46:42 -07:00
|
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-14 09:58:49 -07:00
|
|
|
setState(() {
|
|
|
|
_currentStep = step;
|
|
|
|
});
|
2023-12-07 15:37:29 -07:00
|
|
|
switch (_currentStep) {
|
2023-11-14 09:58:49 -07:00
|
|
|
case CardEntryStep.number:
|
2023-11-14 15:09:03 -07:00
|
|
|
cardNumberFocusNode.requestFocus();
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.exp:
|
2023-11-14 15:09:03 -07:00
|
|
|
expirationFocusNode.requestFocus();
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.cvc:
|
2023-11-14 15:09:03 -07:00
|
|
|
securityCodeFocusNode.requestFocus();
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.postal:
|
2023-11-14 15:09:03 -07:00
|
|
|
postalCodeFocusNode.requestFocus();
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
}
|
2023-12-07 15:38:08 -07:00
|
|
|
|
2023-12-07 16:23:11 -07:00
|
|
|
// Make the selection adjustment only on web, other platforms dont select on focus change
|
2023-12-07 16:24:17 -07:00
|
|
|
if (kIsWeb) {
|
2023-12-07 15:38:08 -07:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _adjustSelection());
|
2023-12-07 16:24:17 -07:00
|
|
|
}
|
2023-12-07 15:37:29 -07:00
|
|
|
|
2023-11-17 16:17:28 -07:00
|
|
|
if (!isWideFormat) {
|
2023-11-14 09:58:49 -07:00
|
|
|
_scrollRow(step);
|
|
|
|
}
|
2023-11-21 09:45:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if any field in the `CardTextField` has focus.
|
2023-12-07 15:37:29 -07:00
|
|
|
bool _anyHaveFocus() {
|
2023-11-21 09:45:25 -07:00
|
|
|
return cardNumberFocusNode.hasFocus ||
|
|
|
|
expirationFocusNode.hasFocus ||
|
|
|
|
securityCodeFocusNode.hasFocus ||
|
|
|
|
postalCodeFocusNode.hasFocus;
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
/// 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;
|
2023-12-07 15:38:08 -07:00
|
|
|
_cardNumberController.value = _cardNumberController.value.copyWith(
|
2023-12-07 16:23:11 -07:00
|
|
|
selection: TextSelection(baseOffset: len, extentOffset: len));
|
2023-12-07 15:37:29 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.exp:
|
|
|
|
final len = _expirationController.text.length;
|
2023-12-07 15:38:08 -07:00
|
|
|
_expirationController.value = _expirationController.value.copyWith(
|
2023-12-07 16:23:11 -07:00
|
|
|
selection: TextSelection(baseOffset: len, extentOffset: len));
|
2023-12-07 15:37:29 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.cvc:
|
|
|
|
final len = _securityCodeController.text.length;
|
2023-12-07 15:38:08 -07:00
|
|
|
_securityCodeController.value = _securityCodeController.value.copyWith(
|
2023-12-07 16:23:11 -07:00
|
|
|
selection: TextSelection(baseOffset: len, extentOffset: len));
|
2023-12-07 15:37:29 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.postal:
|
|
|
|
final len = _postalCodeController.text.length;
|
2023-12-07 15:38:08 -07:00
|
|
|
_postalCodeController.value = _postalCodeController.value.copyWith(
|
2023-12-07 16:23:11 -07:00
|
|
|
selection: TextSelection(baseOffset: len, extentOffset: len));
|
2023-12-07 15:37:29 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Function that is listening to the keyboard events.
|
|
|
|
///
|
|
|
|
/// This provides the functionality of hitting backspace
|
|
|
|
/// and the focus changing between fields when the current
|
|
|
|
/// entry step is empty.
|
2023-11-14 09:58:49 -07:00
|
|
|
void _backspaceTransitionListener(RawKeyEvent value) {
|
|
|
|
if (!value.isKeyPressed(LogicalKeyboardKey.backspace)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
switch (_currentStep) {
|
|
|
|
case CardEntryStep.number:
|
2023-12-07 15:37:29 -07:00
|
|
|
return;
|
2023-11-14 09:58:49 -07:00
|
|
|
case CardEntryStep.exp:
|
2023-11-22 10:52:52 -07:00
|
|
|
if (_expirationController.text.isNotEmpty) return;
|
2023-11-21 09:45:25 -07:00
|
|
|
case CardEntryStep.cvc:
|
2023-11-22 10:52:52 -07:00
|
|
|
if (_securityCodeController.text.isNotEmpty) return;
|
2023-11-21 09:45:25 -07:00
|
|
|
case CardEntryStep.postal:
|
2023-11-22 10:52:52 -07:00
|
|
|
if (_postalCodeController.text.isNotEmpty) return;
|
2023-11-21 09:45:25 -07:00
|
|
|
}
|
|
|
|
_transitionStepFocus();
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:37:29 -07:00
|
|
|
/// Called whenever a text field is emptied and the mobile flag is set
|
|
|
|
void _mobileBackspaceDetected() {
|
|
|
|
// Put the empty char back into the controller to detect backspace on mobile
|
2023-11-21 09:45:25 -07:00
|
|
|
switch (_currentStep) {
|
|
|
|
case CardEntryStep.number:
|
|
|
|
break;
|
|
|
|
case CardEntryStep.exp:
|
|
|
|
_expirationController.text = '\u200b';
|
|
|
|
case CardEntryStep.cvc:
|
|
|
|
_securityCodeController.text = '\u200b';
|
|
|
|
case CardEntryStep.postal:
|
|
|
|
_postalCodeController.text = '\u200b';
|
|
|
|
}
|
|
|
|
_transitionStepFocus();
|
|
|
|
}
|
|
|
|
|
|
|
|
void _transitionStepFocus() {
|
|
|
|
switch (_currentStep) {
|
|
|
|
case CardEntryStep.number:
|
|
|
|
break;
|
|
|
|
case CardEntryStep.exp:
|
2023-11-14 09:58:49 -07:00
|
|
|
_currentCardEntryStepController.add(CardEntryStep.number);
|
2023-12-07 15:37:29 -07:00
|
|
|
|
|
|
|
final String numStr = _cardNumberController.text;
|
|
|
|
final endIndex = numStr.isEmpty ? 0 : numStr.length - 1;
|
|
|
|
_cardNumberController.text = numStr.substring(0, endIndex);
|
2023-11-14 09:58:49 -07:00
|
|
|
break;
|
|
|
|
case CardEntryStep.cvc:
|
|
|
|
_currentCardEntryStepController.add(CardEntryStep.exp);
|
2023-12-07 15:37:29 -07:00
|
|
|
final String expStr = _expirationController.text;
|
|
|
|
final endIndex = expStr.isEmpty ? 0 : expStr.length - 1;
|
|
|
|
_expirationController.text = expStr.substring(0, endIndex);
|
2023-11-21 09:45:25 -07:00
|
|
|
break;
|
2023-11-14 09:58:49 -07:00
|
|
|
case CardEntryStep.postal:
|
|
|
|
_currentCardEntryStepController.add(CardEntryStep.cvc);
|
|
|
|
final String cvcStr = _securityCodeController.text;
|
2023-12-07 15:37:29 -07:00
|
|
|
final endIndex = cvcStr.isEmpty ? 0 : cvcStr.length - 1;
|
|
|
|
_securityCodeController.text = cvcStr.substring(0, endIndex);
|
2023-11-21 09:45:25 -07:00
|
|
|
break;
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Formatter that adds the appropriate space ' ' characters
|
|
|
|
/// to make the card number display cleanly.
|
2023-11-14 09:58:49 -07:00
|
|
|
class CardNumberInputFormatter implements TextInputFormatter {
|
|
|
|
@override
|
2023-12-07 15:38:08 -07:00
|
|
|
TextEditingValue formatEditUpdate(
|
|
|
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
2023-11-14 09:58:49 -07:00
|
|
|
String cardNum = newValue.text;
|
|
|
|
if (cardNum.length <= 4) return newValue;
|
|
|
|
|
|
|
|
cardNum = cardNum.replaceAll(' ', '');
|
|
|
|
StringBuffer buffer = StringBuffer();
|
|
|
|
|
|
|
|
for (int i = 0; i < cardNum.length; i++) {
|
|
|
|
buffer.write(cardNum[i]);
|
|
|
|
int nonZeroIndex = i + 1;
|
|
|
|
if (nonZeroIndex % 4 == 0 && nonZeroIndex != cardNum.length) {
|
|
|
|
buffer.write(' ');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-07 15:38:08 -07:00
|
|
|
return newValue.copyWith(
|
|
|
|
text: buffer.toString(),
|
|
|
|
selection: TextSelection.collapsed(offset: buffer.length));
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 17:32:28 -07:00
|
|
|
/// Formatter that adds a backslash '/' character in between
|
2023-11-14 17:34:11 -07:00
|
|
|
/// the month and the year for the expiration date.
|
2023-11-14 09:58:49 -07:00
|
|
|
class CardExpirationFormatter implements TextInputFormatter {
|
|
|
|
@override
|
2023-12-07 15:38:08 -07:00
|
|
|
TextEditingValue formatEditUpdate(
|
|
|
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
2023-11-14 09:58:49 -07:00
|
|
|
String cardExp = newValue.text;
|
|
|
|
if (cardExp.length == 1) {
|
|
|
|
if (cardExp[0] == '0' || cardExp[0] == '1') {
|
|
|
|
return newValue;
|
|
|
|
} else {
|
|
|
|
cardExp = '0$cardExp';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (cardExp.length == 2 && oldValue.text.length == 3) return newValue;
|
|
|
|
|
|
|
|
cardExp = cardExp.replaceAll('/', '');
|
|
|
|
StringBuffer buffer = StringBuffer();
|
|
|
|
|
|
|
|
for (int i = 0; i < cardExp.length; i++) {
|
|
|
|
buffer.write(cardExp[i]);
|
|
|
|
int nonZeroIndex = i + 1;
|
|
|
|
if (nonZeroIndex == 2) {
|
|
|
|
buffer.write('/');
|
|
|
|
}
|
|
|
|
}
|
2023-12-07 15:38:08 -07:00
|
|
|
return newValue.copyWith(
|
|
|
|
text: buffer.toString(),
|
|
|
|
selection: TextSelection.collapsed(offset: buffer.length));
|
2023-11-14 09:58:49 -07:00
|
|
|
}
|
2023-11-08 15:42:09 -07:00
|
|
|
}
|