WIP
This commit is contained in:
		
							parent
							
								
									41da951be1
								
							
						
					
					
						commit
						88020f4d15
					
				
							
								
								
									
										295
									
								
								lib/card_details.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								lib/card_details.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,295 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CardDetails {
 | 
				
			||||||
 | 
					  CardDetails({required this.cardNumber, required String? securityCode, required this.expirationDate}) {
 | 
				
			||||||
 | 
					    this.securityCode = int.tryParse(securityCode ?? '');
 | 
				
			||||||
 | 
					    checkIsValid();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory CardDetails.blank() {
 | 
				
			||||||
 | 
					    return CardDetails(cardNumber: null, securityCode: null, expirationDate: null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String? cardNumber;
 | 
				
			||||||
 | 
					  int? securityCode;
 | 
				
			||||||
 | 
					  String? postalCode;
 | 
				
			||||||
 | 
					  String? expirationDate;
 | 
				
			||||||
 | 
					  bool _complete = false;
 | 
				
			||||||
 | 
					  ValidState _validState = ValidState.blank;
 | 
				
			||||||
 | 
					  int _lastCheckHash = 0;
 | 
				
			||||||
 | 
					  CardProvider? provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ValidState get validState {
 | 
				
			||||||
 | 
					    checkIsValid();
 | 
				
			||||||
 | 
					    return _validState;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get cardNumberFilled => provider == null || provider?.cardLength == cardNumber?.replaceAll(' ', '').length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get isComplete {
 | 
				
			||||||
 | 
					    checkIsValid();
 | 
				
			||||||
 | 
					    return _complete;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get minInnLength => 1;
 | 
				
			||||||
 | 
					  int get maxINNLength => 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void checkIsValid() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      int currentHash = hash;
 | 
				
			||||||
 | 
					      if (currentHash == _lastCheckHash) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _lastCheckHash = currentHash;
 | 
				
			||||||
 | 
					      if (cardNumber == null && expirationDate == null && securityCode == null && postalCode == null) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.blank;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      final nums = cardNumber!
 | 
				
			||||||
 | 
					          .replaceAll(' ', '')
 | 
				
			||||||
 | 
					          .split('')
 | 
				
			||||||
 | 
					          .map(
 | 
				
			||||||
 | 
					            (i) => int.parse(i),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          .toList();
 | 
				
			||||||
 | 
					      if (!luhnAlgorithmCheck(nums)) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.invalidCard;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (cardNumber == null || !cardNumberFilled) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.missingCard;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (expirationDate == null) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.missingDate;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      final expSplits = expirationDate!.split('/');
 | 
				
			||||||
 | 
					      if (expSplits.length != 2 || expSplits.last == '') {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.missingDate;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      final date = DateTime(2000 + int.parse(expSplits.last),
 | 
				
			||||||
 | 
					          int.parse(expSplits.first[0] == '0' ? expSplits.first[1] : expSplits.first));
 | 
				
			||||||
 | 
					      if (date.isBefore(DateTime.now())) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.dateTooEarly;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      } else if (date.isAfter(DateTime.now().add(const Duration(days: 365 * 50)))) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.dateTooLate;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (securityCode == null) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.missingCVC;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (postalCode == null) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.missingZip;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!RegExp(r'^\d{5}(-\d{4})?$').hasMatch(postalCode!)) {
 | 
				
			||||||
 | 
					        _complete = false;
 | 
				
			||||||
 | 
					        _validState = ValidState.invalidZip;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      _complete = true;
 | 
				
			||||||
 | 
					      _validState = ValidState.ok;
 | 
				
			||||||
 | 
					    } catch (err, st) {
 | 
				
			||||||
 | 
					      if (kDebugMode) {
 | 
				
			||||||
 | 
					        print('Error while validating CardDetails: $err\n$st');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      _complete = false;
 | 
				
			||||||
 | 
					      _validState = ValidState.error;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get hash {
 | 
				
			||||||
 | 
					    return Object.hash(cardNumber, expirationDate, securityCode, postalCode);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void detectCardProvider() {
 | 
				
			||||||
 | 
					    bool found = false;
 | 
				
			||||||
 | 
					    if (cardNumber == null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (var cardPvd in providers) {
 | 
				
			||||||
 | 
					      if (cardPvd.INN_VALID_NUMS != null) {
 | 
				
			||||||
 | 
					        // trim card number to correct length
 | 
				
			||||||
 | 
					        String trimmedNum = cardNumber!;
 | 
				
			||||||
 | 
					        String innNumStr = '${cardPvd.INN_VALID_NUMS!.first}';
 | 
				
			||||||
 | 
					        if (trimmedNum.length > innNumStr.length) {
 | 
				
			||||||
 | 
					          trimmedNum = trimmedNum.substring(0, innNumStr.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        final num = int.tryParse(trimmedNum);
 | 
				
			||||||
 | 
					        if (num == null) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (cardPvd.INN_VALID_NUMS!.contains(num)) {
 | 
				
			||||||
 | 
					          provider = cardPvd;
 | 
				
			||||||
 | 
					          found = true;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (cardPvd.INN_VALID_RANGES != null) {
 | 
				
			||||||
 | 
					        // trim card number to correct length
 | 
				
			||||||
 | 
					        String trimmedNum = cardNumber!;
 | 
				
			||||||
 | 
					        String innNumStr = '${cardPvd.INN_VALID_RANGES!.first.low}';
 | 
				
			||||||
 | 
					        if (trimmedNum.length > innNumStr.length) {
 | 
				
			||||||
 | 
					          trimmedNum = trimmedNum.substring(0, innNumStr.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        final num = int.tryParse(trimmedNum);
 | 
				
			||||||
 | 
					        if (num == null) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (cardPvd.INN_VALID_RANGES!.any((range) => range.isWithin(num))) {
 | 
				
			||||||
 | 
					          provider = cardPvd;
 | 
				
			||||||
 | 
					          found = true;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!found) provider = null;
 | 
				
			||||||
 | 
					    // print('Got provider $provider');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					    String toString() {
 | 
				
			||||||
 | 
					      return 'Number: "$cardNumber" - Exp: "$expirationDate" CVC: $securityCode Zip: "$postalCode"';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum ValidState {
 | 
				
			||||||
 | 
					  ok,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					  blank,
 | 
				
			||||||
 | 
					  missingCard,
 | 
				
			||||||
 | 
					  invalidCard,
 | 
				
			||||||
 | 
					  missingDate,
 | 
				
			||||||
 | 
					  dateTooEarly,
 | 
				
			||||||
 | 
					  dateTooLate,
 | 
				
			||||||
 | 
					  missingCVC,
 | 
				
			||||||
 | 
					  invalidCVC,
 | 
				
			||||||
 | 
					  missingZip,
 | 
				
			||||||
 | 
					  invalidZip,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum CardProviderID {
 | 
				
			||||||
 | 
					  AmericanExpress,
 | 
				
			||||||
 | 
					  DinersClub,
 | 
				
			||||||
 | 
					  DiscoverCard,
 | 
				
			||||||
 | 
					  Mastercard,
 | 
				
			||||||
 | 
					  JCB,
 | 
				
			||||||
 | 
					  Visa,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CardProvider {
 | 
				
			||||||
 | 
					  CardProviderID id;
 | 
				
			||||||
 | 
					  List<int>? INN_VALID_NUMS;
 | 
				
			||||||
 | 
					  List<Range>? INN_VALID_RANGES;
 | 
				
			||||||
 | 
					  int cardLength;
 | 
				
			||||||
 | 
					  int cvcLength;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					      {required this.id,
 | 
				
			||||||
 | 
					      required this.cardLength,
 | 
				
			||||||
 | 
					      required this.cvcLength,
 | 
				
			||||||
 | 
					      this.INN_VALID_NUMS,
 | 
				
			||||||
 | 
					      this.INN_VALID_RANGES}) {
 | 
				
			||||||
 | 
					    // Must provide one or the other
 | 
				
			||||||
 | 
					    assert(INN_VALID_NUMS != null || INN_VALID_RANGES != null);
 | 
				
			||||||
 | 
					    // Do not provide empty list of valid nums
 | 
				
			||||||
 | 
					    assert(INN_VALID_NUMS == null || INN_VALID_NUMS!.isNotEmpty);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return id.toString();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Range {
 | 
				
			||||||
 | 
					  int high;
 | 
				
			||||||
 | 
					  int low;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Range({required this.low, required this.high}) {
 | 
				
			||||||
 | 
					    assert(low <= high);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool isWithin(int val) {
 | 
				
			||||||
 | 
					    return low <= val && val <= high;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					List<CardProvider> providers = [
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.AmericanExpress,
 | 
				
			||||||
 | 
					    cardLength: 15,
 | 
				
			||||||
 | 
					    cvcLength: 4,
 | 
				
			||||||
 | 
					    INN_VALID_NUMS: [34, 37],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.DinersClub,
 | 
				
			||||||
 | 
					    cardLength: 16,
 | 
				
			||||||
 | 
					    cvcLength: 3,
 | 
				
			||||||
 | 
					    INN_VALID_NUMS: [30, 36, 38, 39],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.DiscoverCard,
 | 
				
			||||||
 | 
					    cardLength: 16,
 | 
				
			||||||
 | 
					    cvcLength: 3,
 | 
				
			||||||
 | 
					    INN_VALID_NUMS: [60, 65],
 | 
				
			||||||
 | 
					    INN_VALID_RANGES: [Range(low: 644, high: 649)],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.JCB,
 | 
				
			||||||
 | 
					    cardLength: 16,
 | 
				
			||||||
 | 
					    cvcLength: 3,
 | 
				
			||||||
 | 
					    INN_VALID_NUMS: [35],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.Mastercard,
 | 
				
			||||||
 | 
					    cardLength: 16,
 | 
				
			||||||
 | 
					    cvcLength: 3,
 | 
				
			||||||
 | 
					    INN_VALID_RANGES: [Range(low: 22, high: 27), Range(low: 51, high: 55)],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  CardProvider(
 | 
				
			||||||
 | 
					    id: CardProviderID.Visa,
 | 
				
			||||||
 | 
					    cardLength: 16,
 | 
				
			||||||
 | 
					    cvcLength: 3,
 | 
				
			||||||
 | 
					    INN_VALID_NUMS: [4],
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://en.wikipedia.org/wiki/Luhn_algorithm
 | 
				
			||||||
 | 
					// The Luhn algorithm is used in industry to check
 | 
				
			||||||
 | 
					// for valid credit / debit card numbers
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The algorithm adds together all the numbers, every
 | 
				
			||||||
 | 
					// other number is doubled, then the sum is checked to
 | 
				
			||||||
 | 
					// see if it is a multiple of 10.
 | 
				
			||||||
 | 
					bool luhnAlgorithmCheck(List<int> digits) {
 | 
				
			||||||
 | 
					  int sum = 0;
 | 
				
			||||||
 | 
					  bool isSecond = false;
 | 
				
			||||||
 | 
					  for (int i = digits.length - 1; i >= 0; i--) {
 | 
				
			||||||
 | 
					    int d = digits[i];
 | 
				
			||||||
 | 
					    if (isSecond) {
 | 
				
			||||||
 | 
					      d *= 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (d > 9) {
 | 
				
			||||||
 | 
					        d -= 9;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sum += d;
 | 
				
			||||||
 | 
					    isSecond = !isSecond;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (sum % 10) == 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								lib/card_provider_icon.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								lib/card_provider_icon.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,7 +1,499 @@
 | 
				
			|||||||
library stripe_native_card_field;
 | 
					library stripe_native_card_field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// A Calculator.
 | 
					import 'dart:async';
 | 
				
			||||||
class Calculator {
 | 
					import 'card_details.dart';
 | 
				
			||||||
  /// Returns [value] plus 1.
 | 
					import 'card_provider_icon.dart';
 | 
				
			||||||
  int addOne(int value) => value + 1;
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum CardEntryStep { number, exp, cvc, postal }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CardTextField extends StatefulWidget {
 | 
				
			||||||
 | 
					  const CardTextField({Key? key, required this.width, this.height, this.inputDecoration}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final InputDecoration? inputDecoration;
 | 
				
			||||||
 | 
					  final double width;
 | 
				
			||||||
 | 
					  final double? height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<CardTextField> createState() => _CardTextFieldState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _CardTextFieldState extends State<CardTextField> {
 | 
				
			||||||
 | 
					  late TextEditingController _cardNumberController;
 | 
				
			||||||
 | 
					  late TextEditingController _expirationController;
 | 
				
			||||||
 | 
					  late TextEditingController _securityCodeController;
 | 
				
			||||||
 | 
					  late TextEditingController _postalCodeController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late FocusNode _cardNumberFocusNode;
 | 
				
			||||||
 | 
					  late FocusNode _expirationFocusNode;
 | 
				
			||||||
 | 
					  late FocusNode _securityCodeFocusNode;
 | 
				
			||||||
 | 
					  late FocusNode _postalCodeFocusNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final double _cardFieldWidth = 180.0;
 | 
				
			||||||
 | 
					  final double _expirationFieldWidth = 70.0;
 | 
				
			||||||
 | 
					  final double _securityFieldWidth = 40.0;
 | 
				
			||||||
 | 
					  final double _postalFieldWidth = 100.0;
 | 
				
			||||||
 | 
					  late final double _internalFieldWidth;
 | 
				
			||||||
 | 
					  late final bool _isWideFormat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _showBorderError = false;
 | 
				
			||||||
 | 
					  String? _validationErrorText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _currentCardEntryStepController = StreamController<CardEntryStep>();
 | 
				
			||||||
 | 
					  final _horizontalScrollController = ScrollController();
 | 
				
			||||||
 | 
					  CardEntryStep _currentStep = CardEntryStep.number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _formFieldKey = GlobalKey<FormState>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final CardDetails _cardDetails = CardDetails.blank();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final normalBoxDecoration = BoxDecoration(
 | 
				
			||||||
 | 
					    color: Color(0xfff6f9fc),
 | 
				
			||||||
 | 
					    border: Border.all(
 | 
				
			||||||
 | 
					      color: Color(0xffdde0e3),
 | 
				
			||||||
 | 
					      width: 2.0,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    borderRadius: BorderRadius.circular(8.0),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final errorBoxDecoration = BoxDecoration(
 | 
				
			||||||
 | 
					    color: Color(0xfff6f9fc),
 | 
				
			||||||
 | 
					    border: Border.all(
 | 
				
			||||||
 | 
					      color: Colors.red,
 | 
				
			||||||
 | 
					      width: 2.0,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    borderRadius: BorderRadius.circular(8.0),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final TextStyle _errorTextStyle = TextStyle(color: Colors.red, fontSize: 14);
 | 
				
			||||||
 | 
					  final TextStyle _normalTextStyle = TextStyle(color: Colors.black87, fontSize: 14);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    _cardNumberController = TextEditingController();
 | 
				
			||||||
 | 
					    _expirationController = TextEditingController();
 | 
				
			||||||
 | 
					    _securityCodeController = TextEditingController();
 | 
				
			||||||
 | 
					    _postalCodeController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _cardNumberFocusNode = FocusNode();
 | 
				
			||||||
 | 
					    _expirationFocusNode = FocusNode();
 | 
				
			||||||
 | 
					    _securityCodeFocusNode = FocusNode();
 | 
				
			||||||
 | 
					    _postalCodeFocusNode = FocusNode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _currentCardEntryStepController.stream.listen(
 | 
				
			||||||
 | 
					      _onStepChange,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    RawKeyboard.instance.addListener(_backspaceTransitionListener);
 | 
				
			||||||
 | 
					    _isWideFormat = widget.width >= 450;
 | 
				
			||||||
 | 
					    if (_isWideFormat) {
 | 
				
			||||||
 | 
					      _internalFieldWidth = widget.width + 80;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _internalFieldWidth = _cardFieldWidth + _expirationFieldWidth + _securityFieldWidth + _postalFieldWidth + 80;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _cardNumberController.dispose();
 | 
				
			||||||
 | 
					    _expirationController.dispose();
 | 
				
			||||||
 | 
					    _securityCodeController.dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _cardNumberFocusNode.dispose();
 | 
				
			||||||
 | 
					    _expirationFocusNode.dispose();
 | 
				
			||||||
 | 
					    _securityCodeFocusNode.dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    RawKeyboard.instance.removeListener(_backspaceTransitionListener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Form(
 | 
				
			||||||
 | 
					          key: _formFieldKey,
 | 
				
			||||||
 | 
					          child: GestureDetector(
 | 
				
			||||||
 | 
					            onTap: () {
 | 
				
			||||||
 | 
					              // Focuses to the current field
 | 
				
			||||||
 | 
					              _currentCardEntryStepController.add(_currentStep);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            child: Container(
 | 
				
			||||||
 | 
					              width: widget.width,
 | 
				
			||||||
 | 
					              height: widget.height ?? 60.0,
 | 
				
			||||||
 | 
					              decoration: _showBorderError ? errorBoxDecoration : normalBoxDecoration,
 | 
				
			||||||
 | 
					              child: ClipRect(
 | 
				
			||||||
 | 
					                child: IgnorePointer(
 | 
				
			||||||
 | 
					                  child: SingleChildScrollView(
 | 
				
			||||||
 | 
					                    controller: _horizontalScrollController,
 | 
				
			||||||
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                    child: SizedBox(
 | 
				
			||||||
 | 
					                      width: _internalFieldWidth,
 | 
				
			||||||
 | 
					                      height: widget.height ?? 60.0,
 | 
				
			||||||
 | 
					                      child: Row(
 | 
				
			||||||
 | 
					                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                        mainAxisAlignment: MainAxisAlignment.start,
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          Padding(
 | 
				
			||||||
 | 
					                            padding: const EdgeInsets.symmetric(horizontal: 6.0),
 | 
				
			||||||
 | 
					                            child: CardProviderIcon(
 | 
				
			||||||
 | 
					                              cardDetails: _cardDetails,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          Container(
 | 
				
			||||||
 | 
					                            width: _cardFieldWidth,
 | 
				
			||||||
 | 
					                            child: TextFormField(
 | 
				
			||||||
 | 
					                              focusNode: _cardNumberFocusNode,
 | 
				
			||||||
 | 
					                              controller: _cardNumberController,
 | 
				
			||||||
 | 
					                              keyboardType: TextInputType.number,
 | 
				
			||||||
 | 
					                              style: _isRedText([ValidState.invalidCard, ValidState.missingCard, ValidState.blank])
 | 
				
			||||||
 | 
					                                  ? _errorTextStyle
 | 
				
			||||||
 | 
					                                  : _normalTextStyle,
 | 
				
			||||||
 | 
					                              validator: (content) {
 | 
				
			||||||
 | 
					                                if (content == null || content.isEmpty) {
 | 
				
			||||||
 | 
					                                  return null;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                _cardDetails.cardNumber = content;
 | 
				
			||||||
 | 
					                                if (_cardDetails.validState == ValidState.invalidCard) {
 | 
				
			||||||
 | 
					                                  _setValidationState('You card number is invalid.');
 | 
				
			||||||
 | 
					                                } else if (_cardDetails.validState == ValidState.missingCard) {
 | 
				
			||||||
 | 
					                                  _setValidationState('Your card number is incomplete.');
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return null;
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              onChanged: (str) {
 | 
				
			||||||
 | 
					                                final numbers = str.replaceAll(' ', '');
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.cardNumber = numbers);
 | 
				
			||||||
 | 
					                                if (str.length <= _cardDetails.maxINNLength) {
 | 
				
			||||||
 | 
					                                  _cardDetails.detectCardProvider();
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                if (numbers.length == 16) {
 | 
				
			||||||
 | 
					                                  _currentCardEntryStepController.add(CardEntryStep.exp);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              inputFormatters: [
 | 
				
			||||||
 | 
					                                LengthLimitingTextInputFormatter(19),
 | 
				
			||||||
 | 
					                                FilteringTextInputFormatter.allow(RegExp('[0-9 ]')),
 | 
				
			||||||
 | 
					                                CardNumberInputFormatter(),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                              decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                hintText: 'Card number',
 | 
				
			||||||
 | 
					                                fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					                                border: InputBorder.none,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          if (_isWideFormat)
 | 
				
			||||||
 | 
					                            Flexible(
 | 
				
			||||||
 | 
					                                fit: FlexFit.loose,
 | 
				
			||||||
 | 
					                                // fit: _currentStep == CardEntryStep.number ? FlexFit.loose : FlexFit.tight,
 | 
				
			||||||
 | 
					                                child: AnimatedContainer(
 | 
				
			||||||
 | 
					                                    curve: Curves.easeOut,
 | 
				
			||||||
 | 
					                                    duration: const Duration(milliseconds: 400),
 | 
				
			||||||
 | 
					                                    constraints: _currentStep == CardEntryStep.number
 | 
				
			||||||
 | 
					                                        ? BoxConstraints.loose(Size(400.0, 1.0))
 | 
				
			||||||
 | 
					                                        : BoxConstraints.tight(Size(0, 0)))),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                          // Spacer(flex: _currentStep == CardEntryStep.number ? 100 : 1),
 | 
				
			||||||
 | 
					                          AnimatedContainer(
 | 
				
			||||||
 | 
					                            duration: const Duration(milliseconds: 125),
 | 
				
			||||||
 | 
					                            width: _expirationFieldWidth,
 | 
				
			||||||
 | 
					                            child: TextFormField(
 | 
				
			||||||
 | 
					                              focusNode: _expirationFocusNode,
 | 
				
			||||||
 | 
					                              controller: _expirationController,
 | 
				
			||||||
 | 
					                              style:
 | 
				
			||||||
 | 
					                                  _isRedText([ValidState.dateTooLate, ValidState.dateTooEarly, ValidState.missingDate])
 | 
				
			||||||
 | 
					                                      ? _errorTextStyle
 | 
				
			||||||
 | 
					                                      : _normalTextStyle,
 | 
				
			||||||
 | 
					                              validator: (content) {
 | 
				
			||||||
 | 
					                                if (content == null || content.isEmpty) {
 | 
				
			||||||
 | 
					                                  return null;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.expirationDate = content);
 | 
				
			||||||
 | 
					                                if (_cardDetails.validState == ValidState.dateTooEarly) {
 | 
				
			||||||
 | 
					                                  _setValidationState('Your card\'s expiration date is in the past.');
 | 
				
			||||||
 | 
					                                } else if (_cardDetails.validState == ValidState.dateTooLate) {
 | 
				
			||||||
 | 
					                                  _setValidationState('Your card\'s expiration year is invalid.');
 | 
				
			||||||
 | 
					                                } else if (_cardDetails.validState == ValidState.missingDate) {
 | 
				
			||||||
 | 
					                                  _setValidationState('You must include your card\'s expiration date.');
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return null;
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              onChanged: (str) {
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.expirationDate = str);
 | 
				
			||||||
 | 
					                                if (str.length == 5) {
 | 
				
			||||||
 | 
					                                  _currentCardEntryStepController.add(CardEntryStep.cvc);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              inputFormatters: [
 | 
				
			||||||
 | 
					                                LengthLimitingTextInputFormatter(5),
 | 
				
			||||||
 | 
					                                FilteringTextInputFormatter.allow(RegExp('[0-9/]')),
 | 
				
			||||||
 | 
					                                CardExpirationFormatter(),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                              decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                hintText: 'MM/YY',
 | 
				
			||||||
 | 
					                                fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					                                border: InputBorder.none,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          AnimatedContainer(
 | 
				
			||||||
 | 
					                            duration: const Duration(milliseconds: 250),
 | 
				
			||||||
 | 
					                            width: _securityFieldWidth,
 | 
				
			||||||
 | 
					                            child: TextFormField(
 | 
				
			||||||
 | 
					                              focusNode: _securityCodeFocusNode,
 | 
				
			||||||
 | 
					                              controller: _securityCodeController,
 | 
				
			||||||
 | 
					                              style: _isRedText([ValidState.invalidCVC, ValidState.missingCVC])
 | 
				
			||||||
 | 
					                                  ? _errorTextStyle
 | 
				
			||||||
 | 
					                                  : _normalTextStyle,
 | 
				
			||||||
 | 
					                              validator: (content) {
 | 
				
			||||||
 | 
					                                if (content == null || content.isEmpty) {
 | 
				
			||||||
 | 
					                                  return null;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.securityCode = int.tryParse(content));
 | 
				
			||||||
 | 
					                                if (_cardDetails.validState == ValidState.invalidCVC) {
 | 
				
			||||||
 | 
					                                  _setValidationState('Your card\'s security code is invalid.');
 | 
				
			||||||
 | 
					                                } else if (_cardDetails.validState == ValidState.missingCVC) {
 | 
				
			||||||
 | 
					                                  _setValidationState('You card\'s security code is incomplete.');
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return null;
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              onChanged: (str) {
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.expirationDate = str);
 | 
				
			||||||
 | 
					                                if (str.length == _cardDetails.provider?.cvcLength) {
 | 
				
			||||||
 | 
					                                  _currentCardEntryStepController.add(CardEntryStep.postal);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              inputFormatters: [
 | 
				
			||||||
 | 
					                                LengthLimitingTextInputFormatter(
 | 
				
			||||||
 | 
					                                    _cardDetails.provider == null ? 4 : _cardDetails.provider!.cvcLength),
 | 
				
			||||||
 | 
					                                FilteringTextInputFormatter.allow(RegExp('[0-9]')),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                              decoration: const InputDecoration(
 | 
				
			||||||
 | 
					                                hintText: 'CVC',
 | 
				
			||||||
 | 
					                                fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					                                border: InputBorder.none,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          AnimatedContainer(
 | 
				
			||||||
 | 
					                            duration: const Duration(milliseconds: 250),
 | 
				
			||||||
 | 
					                            width: _postalFieldWidth,
 | 
				
			||||||
 | 
					                            child: TextFormField(
 | 
				
			||||||
 | 
					                              focusNode: _postalCodeFocusNode,
 | 
				
			||||||
 | 
					                              controller: _postalCodeController,
 | 
				
			||||||
 | 
					                              style: _isRedText([ValidState.invalidZip, ValidState.missingZip])
 | 
				
			||||||
 | 
					                                  ? _errorTextStyle
 | 
				
			||||||
 | 
					                                  : _normalTextStyle,
 | 
				
			||||||
 | 
					                              validator: (content) {
 | 
				
			||||||
 | 
					                                print('validate zipcode');
 | 
				
			||||||
 | 
					                                if (content == null || content.isEmpty) {
 | 
				
			||||||
 | 
					                                  return null;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.postalCode = content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					print('checking here\n$_cardDetails');
 | 
				
			||||||
 | 
					                                if (_cardDetails.validState == ValidState.invalidZip) {
 | 
				
			||||||
 | 
					                                  _setValidationState('The postal code you entered is not correct.');
 | 
				
			||||||
 | 
					                                } else if (_cardDetails.validState == ValidState.missingZip) {
 | 
				
			||||||
 | 
					                                  _setValidationState('You must enter your card\'s postal code.');
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return null;
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              onChanged: (str) {
 | 
				
			||||||
 | 
					                                  print('here');
 | 
				
			||||||
 | 
					                                setState(() => _cardDetails.postalCode = str);
 | 
				
			||||||
 | 
					                                print(_cardDetails.toString());
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              onFieldSubmitted: (_) {
 | 
				
			||||||
 | 
					                                  print('finished');
 | 
				
			||||||
 | 
					                                _validateFields();
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                              decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                hintText: _currentStep == CardEntryStep.number ? '' : 'Postal Code',
 | 
				
			||||||
 | 
					                                fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					                                border: InputBorder.none,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        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(
 | 
				
			||||||
 | 
					              _validationErrorText ?? '',
 | 
				
			||||||
 | 
					              style: const TextStyle(color: Colors.red),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isRedText(List<ValidState> args) {
 | 
				
			||||||
 | 
					    return _showBorderError && args.contains(_cardDetails.validState);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setValidationState(String? text) {
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _validationErrorText = text;
 | 
				
			||||||
 | 
					      _showBorderError = text != null;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _validateFields() {
 | 
				
			||||||
 | 
					    _validationErrorText = null;
 | 
				
			||||||
 | 
					    _formFieldKey.currentState!.validate();
 | 
				
			||||||
 | 
					    // Clear up validation state if everything is valid
 | 
				
			||||||
 | 
					    if (_validationErrorText == null) {
 | 
				
			||||||
 | 
					      _setValidationState(null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _scrollRow(CardEntryStep step) {
 | 
				
			||||||
 | 
					    final dur = Duration(milliseconds: 150);
 | 
				
			||||||
 | 
					    final cur = Curves.easeOut;
 | 
				
			||||||
 | 
					    final fieldLen = widget.width;
 | 
				
			||||||
 | 
					    switch (step) {
 | 
				
			||||||
 | 
					      case CardEntryStep.number:
 | 
				
			||||||
 | 
					        _horizontalScrollController.animateTo(0.0, duration: dur, curve: cur);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.exp:
 | 
				
			||||||
 | 
					        _horizontalScrollController.animateTo(_cardFieldWidth / 2, duration: dur, curve: cur);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.cvc:
 | 
				
			||||||
 | 
					        _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth, duration: dur, curve: cur);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.postal:
 | 
				
			||||||
 | 
					        _horizontalScrollController.animateTo(_cardFieldWidth / 2 + _expirationFieldWidth + _securityFieldWidth,
 | 
				
			||||||
 | 
					            duration: dur, curve: cur);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onStepChange(CardEntryStep step) {
 | 
				
			||||||
 | 
					    if (_currentStep.index < step.index) {
 | 
				
			||||||
 | 
					      _validateFields();
 | 
				
			||||||
 | 
					    } else if (_currentStep != step) {
 | 
				
			||||||
 | 
					      _setValidationState(null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _currentStep = step;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    switch (step) {
 | 
				
			||||||
 | 
					      case CardEntryStep.number:
 | 
				
			||||||
 | 
					        _cardNumberFocusNode.requestFocus();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.exp:
 | 
				
			||||||
 | 
					        _expirationFocusNode.requestFocus();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.cvc:
 | 
				
			||||||
 | 
					        _securityCodeFocusNode.requestFocus();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.postal:
 | 
				
			||||||
 | 
					        _postalCodeFocusNode.requestFocus();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!_isWideFormat) {
 | 
				
			||||||
 | 
					      _scrollRow(step);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _backspaceTransitionListener(RawKeyEvent value) {
 | 
				
			||||||
 | 
					    if (!value.isKeyPressed(LogicalKeyboardKey.backspace)) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    switch (_currentStep) {
 | 
				
			||||||
 | 
					      case CardEntryStep.number:
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.exp:
 | 
				
			||||||
 | 
					        final expStr = _expirationController.text;
 | 
				
			||||||
 | 
					        if (expStr.isNotEmpty) break;
 | 
				
			||||||
 | 
					        _currentCardEntryStepController.add(CardEntryStep.number);
 | 
				
			||||||
 | 
					        String numStr = _cardNumberController.text;
 | 
				
			||||||
 | 
					        _cardNumberController.text = numStr.substring(0, numStr.length - 1);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case CardEntryStep.cvc:
 | 
				
			||||||
 | 
					        final cvcStr = _securityCodeController.text;
 | 
				
			||||||
 | 
					        if (cvcStr.isNotEmpty) break;
 | 
				
			||||||
 | 
					        _currentCardEntryStepController.add(CardEntryStep.exp);
 | 
				
			||||||
 | 
					        final expStr = _expirationController.text;
 | 
				
			||||||
 | 
					        _expirationController.text = expStr.substring(0, expStr.length - 1);
 | 
				
			||||||
 | 
					      case CardEntryStep.postal:
 | 
				
			||||||
 | 
					        final String postalStr = _postalCodeController.text;
 | 
				
			||||||
 | 
					        if (postalStr.isNotEmpty) break;
 | 
				
			||||||
 | 
					        _currentCardEntryStepController.add(CardEntryStep.cvc);
 | 
				
			||||||
 | 
					        final String cvcStr = _securityCodeController.text;
 | 
				
			||||||
 | 
					        _securityCodeController.text = cvcStr.substring(0, cvcStr.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CardNumberInputFormatter implements TextInputFormatter {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
 | 
				
			||||||
 | 
					    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(' ');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CardExpirationFormatter implements TextInputFormatter {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					    // Auto delete the slash on backspace
 | 
				
			||||||
 | 
					    // if (cardExp.length == 3 && oldValue.text.length == 4 && cardExp[2] == '/') {
 | 
				
			||||||
 | 
					    //   return newValue.copyWith(
 | 
				
			||||||
 | 
					    //       text: cardExp.substring(0, 2), selection: TextSelection.collapsed(offset: cardExp.length - 1));
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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('/');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return newValue.copyWith(text: buffer.toString(), selection: TextSelection.collapsed(offset: buffer.length));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ environment:
 | 
				
			|||||||
dependencies:
 | 
					dependencies:
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    sdk: flutter
 | 
					    sdk: flutter
 | 
				
			||||||
 | 
					  flutter_svg: ^2.0.9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 | 
				
			|||||||
@ -3,10 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
 | 
				
			|||||||
import 'package:stripe_native_card_field/stripe_native_card_field.dart';
 | 
					import 'package:stripe_native_card_field/stripe_native_card_field.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  test('adds one to input values', () {
 | 
					  test('CardDetails:', () {
 | 
				
			||||||
    final calculator = Calculator();
 | 
					
 | 
				
			||||||
    expect(calculator.addOne(2), 3);
 | 
					 });
 | 
				
			||||||
    expect(calculator.addOne(-7), -6);
 | 
					 | 
				
			||||||
    expect(calculator.addOne(0), 1);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user