Added dartdoc comments for PUB POINTS!!
This commit is contained in:
parent
f9e758fda5
commit
f23805a3a8
2
LICENSE
2
LICENSE
|
@ -1,3 +1,5 @@
|
||||||
|
Copyright 2023 Nathan Anderson
|
||||||
|
|
||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// Class encapsulating the card's data
|
||||||
|
/// as well as validation of the data.
|
||||||
|
///
|
||||||
|
/// `CardDetails.validState == ValidState.ok`
|
||||||
|
/// when fields are filled and validated as correct.
|
||||||
class CardDetails {
|
class CardDetails {
|
||||||
CardDetails({
|
CardDetails({
|
||||||
required dynamic cardNumber,
|
required dynamic cardNumber,
|
||||||
|
@ -11,10 +16,13 @@ class CardDetails {
|
||||||
checkIsValid();
|
checkIsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets every field to null, a default
|
||||||
|
/// `CardDetails` when nothing has been entered.
|
||||||
factory CardDetails.blank() {
|
factory CardDetails.blank() {
|
||||||
return CardDetails(cardNumber: null, securityCode: null, expirationString: null, postalCode: null);
|
return CardDetails(cardNumber: null, securityCode: null, expirationString: null, postalCode: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the CardNumber as a `String` with the spaces removed.
|
||||||
String? get cardNumber => _cardNumber?.replaceAll(' ', '');
|
String? get cardNumber => _cardNumber?.replaceAll(' ', '');
|
||||||
|
|
||||||
set cardNumber(String? num) => _cardNumber = num;
|
set cardNumber(String? num) => _cardNumber = num;
|
||||||
|
@ -29,22 +37,36 @@ class CardDetails {
|
||||||
int _lastCheckHash = 0;
|
int _lastCheckHash = 0;
|
||||||
CardProvider? provider;
|
CardProvider? provider;
|
||||||
|
|
||||||
|
|
||||||
|
/// Checks the validity of the `CardDetails` and returns the result.
|
||||||
ValidState get validState {
|
ValidState get validState {
|
||||||
checkIsValid();
|
checkIsValid();
|
||||||
return _validState;
|
return _validState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO rename to be more clear
|
||||||
|
/// Returns true if `_cardNumber` is null, or
|
||||||
|
/// if the _cardNumber matches the detected `provider`'s
|
||||||
|
/// card lenght, defaulting to 16.
|
||||||
bool get cardNumberFilled =>
|
bool get cardNumberFilled =>
|
||||||
_cardNumber == null ? false : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length;
|
_cardNumber == null ? false : (provider?.cardLength ?? 16) == _cardNumber!.replaceAll(' ', '').length;
|
||||||
|
|
||||||
|
/// Returns true if all details are complete and valid
|
||||||
|
/// otherwise, return false.
|
||||||
bool get isComplete {
|
bool get isComplete {
|
||||||
checkIsValid();
|
checkIsValid();
|
||||||
return _complete;
|
return _complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get minInnLength => 1;
|
/// The maximum length of the INN (identifier)
|
||||||
|
/// of a card provider.
|
||||||
int get maxINNLength => 4;
|
int get maxINNLength => 4;
|
||||||
|
|
||||||
|
/// Validates each field of the `CardDetails` object in entry order,
|
||||||
|
/// namely _cardNumber -> expirationString -> securityCode -> postalCode
|
||||||
|
///
|
||||||
|
/// If all fields are filled out and valid, `CardDetails.isComplete == true`
|
||||||
|
/// and `CardDetails.validState == ValidState.ok`.
|
||||||
void checkIsValid() {
|
void checkIsValid() {
|
||||||
try {
|
try {
|
||||||
int currentHash = hash;
|
int currentHash = hash;
|
||||||
|
@ -65,7 +87,7 @@ class CardDetails {
|
||||||
(i) => int.parse(i),
|
(i) => int.parse(i),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (!luhnAlgorithmCheck(nums)) {
|
if (!_luhnAlgorithmCheck(nums)) {
|
||||||
_complete = false;
|
_complete = false;
|
||||||
_validState = ValidState.invalidCard;
|
_validState = ValidState.invalidCard;
|
||||||
return;
|
return;
|
||||||
|
@ -130,16 +152,21 @@ class CardDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provides a hash of the CardDetails object
|
||||||
|
/// Hashes `_cardNumber`, `expirationString`,
|
||||||
|
/// `securityCode`, and `postalCode`.
|
||||||
int get hash {
|
int get hash {
|
||||||
return Object.hash(_cardNumber, expirationString, securityCode, postalCode);
|
return Object.hash(_cardNumber, expirationString, securityCode, postalCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterates over the list `_providers`, detecting which
|
||||||
|
/// provider the current `_cardNumber` falls under.
|
||||||
void detectCardProvider() {
|
void detectCardProvider() {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
if (_cardNumber == null) {
|
if (_cardNumber == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (var cardPvd in providers) {
|
for (var cardPvd in _providers) {
|
||||||
if (cardPvd.innValidNums != null) {
|
if (cardPvd.innValidNums != null) {
|
||||||
// trim card number to correct length
|
// trim card number to correct length
|
||||||
String trimmedNum = _cardNumber!;
|
String trimmedNum = _cardNumber!;
|
||||||
|
@ -180,8 +207,36 @@ class CardDetails {
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Number: "$_cardNumber" - Exp: "$expirationString" CVC: $securityCode Zip: "$postalCode"';
|
return 'Number: "$_cardNumber" - Exp: "$expirationString" CVC: $securityCode Zip: "$postalCode"';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
/// https://en.wikipedia.org/wiki/Luhn_algorithm
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum of validation states a `CardDetails` object can have.
|
||||||
enum ValidState {
|
enum ValidState {
|
||||||
ok,
|
ok,
|
||||||
error,
|
error,
|
||||||
|
@ -198,6 +253,7 @@ enum ValidState {
|
||||||
invalidZip,
|
invalidZip,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum of supported U.S. Card Providers
|
||||||
enum CardProviderID {
|
enum CardProviderID {
|
||||||
americanExpress,
|
americanExpress,
|
||||||
dinersClub,
|
dinersClub,
|
||||||
|
@ -207,10 +263,13 @@ enum CardProviderID {
|
||||||
visa,
|
visa,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encapsulates criteria for Card Providers in the U.S.
|
||||||
|
/// Used by `CardDetails.detectCardProvider()` to determine
|
||||||
|
/// a card's Provider.
|
||||||
class CardProvider {
|
class CardProvider {
|
||||||
CardProviderID id;
|
CardProviderID id;
|
||||||
List<int>? innValidNums;
|
List<int>? innValidNums;
|
||||||
List<Range>? innValidRanges;
|
List<_Range>? innValidRanges;
|
||||||
int cardLength;
|
int cardLength;
|
||||||
int cvcLength;
|
int cvcLength;
|
||||||
|
|
||||||
|
@ -228,20 +287,30 @@ class CardProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Range {
|
/// Object for `CardProvider` to determine valid number ranges.
|
||||||
|
/// A loose wrapper on a tuple, that provides assertion of
|
||||||
|
/// valid inputs and the `isWithin()` helper function.
|
||||||
|
class _Range {
|
||||||
int high;
|
int high;
|
||||||
int low;
|
int low;
|
||||||
|
|
||||||
Range({required this.low, required this.high}) {
|
_Range({required this.low, required this.high}) {
|
||||||
assert(low <= high);
|
assert(low <= high);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns bool whether or not `val` is between `low` and `high`.
|
||||||
|
/// The range includes the `val`, so
|
||||||
|
/// ```dart
|
||||||
|
/// Range(low: 1, high: 3).isWithin(3);
|
||||||
|
/// ```
|
||||||
|
/// would return true.
|
||||||
bool isWithin(int val) {
|
bool isWithin(int val) {
|
||||||
return low <= val && val <= high;
|
return low <= val && val <= high;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<CardProvider> providers = [
|
/// List of CardProviders for US-based Credit / Debit Cards.
|
||||||
|
List<CardProvider> _providers = [
|
||||||
CardProvider(
|
CardProvider(
|
||||||
id: CardProviderID.americanExpress,
|
id: CardProviderID.americanExpress,
|
||||||
cardLength: 15,
|
cardLength: 15,
|
||||||
|
@ -259,7 +328,7 @@ List<CardProvider> providers = [
|
||||||
cardLength: 16,
|
cardLength: 16,
|
||||||
cvcLength: 3,
|
cvcLength: 3,
|
||||||
innValidNums: [60, 65],
|
innValidNums: [60, 65],
|
||||||
innValidRanges: [Range(low: 644, high: 649)],
|
innValidRanges: [_Range(low: 644, high: 649)],
|
||||||
),
|
),
|
||||||
CardProvider(
|
CardProvider(
|
||||||
id: CardProviderID.jcb,
|
id: CardProviderID.jcb,
|
||||||
|
@ -271,7 +340,7 @@ List<CardProvider> providers = [
|
||||||
id: CardProviderID.mastercard,
|
id: CardProviderID.mastercard,
|
||||||
cardLength: 16,
|
cardLength: 16,
|
||||||
cvcLength: 3,
|
cvcLength: 3,
|
||||||
innValidRanges: [Range(low: 22, high: 27), Range(low: 51, high: 55)],
|
innValidRanges: [_Range(low: 22, high: 27), _Range(low: 51, high: 55)],
|
||||||
),
|
),
|
||||||
CardProvider(
|
CardProvider(
|
||||||
id: CardProviderID.visa,
|
id: CardProviderID.visa,
|
||||||
|
@ -280,29 +349,3 @@ List<CardProvider> providers = [
|
||||||
innValidNums: [4],
|
innValidNums: [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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,10 @@ import 'card_details.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
/// Widget that provides the various supported card provider's
|
||||||
|
/// icons, as well as a default and error card icon.
|
||||||
|
///
|
||||||
|
/// To see a list of supported card providers, see `CardDetails.provider`.
|
||||||
class CardProviderIcon extends StatefulWidget {
|
class CardProviderIcon extends StatefulWidget {
|
||||||
const CardProviderIcon({required this.cardDetails, super.key});
|
const CardProviderIcon({required this.cardDetails, super.key});
|
||||||
|
|
||||||
|
@ -77,6 +81,7 @@ class _CardProviderIconState extends State<CardProviderIcon> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to create the SVG icons provided a `CardProviderID`.
|
||||||
Widget createCardSvg(CardProviderID id) {
|
Widget createCardSvg(CardProviderID id) {
|
||||||
return SvgPicture.string(
|
return SvgPicture.string(
|
||||||
key: Key('${id.name}-card'),
|
key: Key('${id.name}-card'),
|
||||||
|
|
|
@ -6,8 +6,18 @@ import 'card_provider_icon.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Enum to track each step of the card detail
|
||||||
|
/// entry process.
|
||||||
enum CardEntryStep { number, exp, cvc, postal }
|
enum CardEntryStep { number, exp, cvc, postal }
|
||||||
|
|
||||||
|
/// A uniform text field for entering card details, based
|
||||||
|
/// on the behavior of Stripe's various html elements.
|
||||||
|
///
|
||||||
|
/// Required `width` and `onCardDetailsComplete`.
|
||||||
|
///
|
||||||
|
/// If the provided `width < 450.0`, the `CardTextField`
|
||||||
|
/// will scroll its content horizontally with the cursor
|
||||||
|
/// to compensate.
|
||||||
class CardTextField extends StatefulWidget {
|
class CardTextField extends StatefulWidget {
|
||||||
const CardTextField(
|
const CardTextField(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
|
@ -21,8 +31,9 @@ class CardTextField extends StatefulWidget {
|
||||||
|
|
||||||
final InputDecoration? inputDecoration; // TODO unapplied style
|
final InputDecoration? inputDecoration; // TODO unapplied style
|
||||||
final BoxDecoration? boxDecoration; // TODO unapplied style
|
final BoxDecoration? boxDecoration; // TODO unapplied style
|
||||||
final BoxDecoration? errorBoxDecoration; // TODO unapplied style
|
final BoxDecoration? errorBoxDecoration; // TODO unapplied style
|
||||||
final double width;
|
final double width;
|
||||||
|
/// Callback that returns the completed CardDetails object
|
||||||
final void Function(CardDetails) onCardDetailsComplete;
|
final void Function(CardDetails) onCardDetailsComplete;
|
||||||
final double? height;
|
final double? height;
|
||||||
|
|
||||||
|
@ -30,6 +41,9 @@ class CardTextField extends StatefulWidget {
|
||||||
State<CardTextField> createState() => CardTextFieldState();
|
State<CardTextField> createState() => CardTextFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State Widget for CardTextField
|
||||||
|
/// Should not be used directly, create a
|
||||||
|
/// `CardTextField()` instead.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
class CardTextFieldState extends State<CardTextField> {
|
class CardTextFieldState extends State<CardTextField> {
|
||||||
late TextEditingController _cardNumberController;
|
late TextEditingController _cardNumberController;
|
||||||
|
@ -361,10 +375,14 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provided a list of `ValidState`, returns whether
|
||||||
|
/// make the text field red
|
||||||
bool _isRedText(List<ValidState> args) {
|
bool _isRedText(List<ValidState> args) {
|
||||||
return _showBorderError && args.contains(_cardDetails.validState);
|
return _showBorderError && args.contains(_cardDetails.validState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to change the `_showBorderError` and
|
||||||
|
/// `_validationErrorText`.
|
||||||
void _setValidationState(String? text) {
|
void _setValidationState(String? text) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validationErrorText = text;
|
_validationErrorText = text;
|
||||||
|
@ -372,6 +390,8 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls `validate()` on the form state and resets
|
||||||
|
/// the validation state
|
||||||
void _validateFields() {
|
void _validateFields() {
|
||||||
_validationErrorText = null;
|
_validationErrorText = null;
|
||||||
_formFieldKey.currentState!.validate();
|
_formFieldKey.currentState!.validate();
|
||||||
|
@ -382,6 +402,8 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used when `_isWideFormat == false`, scrolls
|
||||||
|
/// the `_horizontalScrollController` to a given offset
|
||||||
void _scrollRow(CardEntryStep step) {
|
void _scrollRow(CardEntryStep step) {
|
||||||
const dur = Duration(milliseconds: 150);
|
const dur = Duration(milliseconds: 150);
|
||||||
const cur = Curves.easeOut;
|
const cur = Curves.easeOut;
|
||||||
|
@ -402,6 +424,9 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Function that is listening to the `_currentCardEntryStepController`
|
||||||
|
/// StreamController. Manages validation and tracking of the current step
|
||||||
|
/// as well as scrolling the text fields.
|
||||||
void _onStepChange(CardEntryStep step) {
|
void _onStepChange(CardEntryStep step) {
|
||||||
if (_currentStep.index < step.index) {
|
if (_currentStep.index < step.index) {
|
||||||
_validateFields();
|
_validateFields();
|
||||||
|
@ -431,6 +456,11 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
void _backspaceTransitionListener(RawKeyEvent value) {
|
void _backspaceTransitionListener(RawKeyEvent value) {
|
||||||
if (!value.isKeyPressed(LogicalKeyboardKey.backspace)) {
|
if (!value.isKeyPressed(LogicalKeyboardKey.backspace)) {
|
||||||
return;
|
return;
|
||||||
|
@ -461,6 +491,8 @@ class CardTextFieldState extends State<CardTextField> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatter that adds the appropriate space ' ' characters
|
||||||
|
/// to make the card number display cleanly.
|
||||||
class CardNumberInputFormatter implements TextInputFormatter {
|
class CardNumberInputFormatter implements TextInputFormatter {
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
@ -482,6 +514,8 @@ class CardNumberInputFormatter implements TextInputFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formatter that adds a backslash '/' character in between
|
||||||
|
/// the month and the year for the expiration date.
|
||||||
class CardExpirationFormatter implements TextInputFormatter {
|
class CardExpirationFormatter implements TextInputFormatter {
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
@ -494,11 +528,6 @@ class CardExpirationFormatter implements TextInputFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cardExp.length == 2 && oldValue.text.length == 3) return newValue;
|
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('/', '');
|
cardExp = cardExp.replaceAll('/', '');
|
||||||
StringBuffer buffer = StringBuffer();
|
StringBuffer buffer = StringBuffer();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user