Added dartdoc comments for PUB POINTS!!

This commit is contained in:
Nathan Anderson 2023-11-14 17:32:28 -07:00
parent f9e758fda5
commit f23805a3a8
4 changed files with 120 additions and 41 deletions

View File

@ -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:

View File

@ -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,115 +207,16 @@ class CardDetails {
String toString() { String toString() {
return 'Number: "$_cardNumber" - Exp: "$expirationString" CVC: $securityCode Zip: "$postalCode"'; return 'Number: "$_cardNumber" - Exp: "$expirationString" CVC: $securityCode Zip: "$postalCode"';
} }
}
enum ValidState { /// https://en.wikipedia.org/wiki/Luhn_algorithm
ok, /// The Luhn algorithm is used in industry to check
error, /// for valid credit / debit card numbers
blank, ///
missingCard, /// The algorithm adds together all the numbers, every
invalidCard, /// other number is doubled, then the sum is checked to
missingDate, /// see if it is a multiple of 10.
invalidMonth, /// https://en.wikipedia.org/wiki/Luhn_algorithm
dateTooEarly, bool _luhnAlgorithmCheck(List<int> digits) {
dateTooLate,
missingCVC,
invalidCVC,
missingZip,
invalidZip,
}
enum CardProviderID {
americanExpress,
dinersClub,
discoverCard,
mastercard,
jcb,
visa,
}
class CardProvider {
CardProviderID id;
List<int>? innValidNums;
List<Range>? innValidRanges;
int cardLength;
int cvcLength;
CardProvider(
{required this.id, required this.cardLength, required this.cvcLength, this.innValidNums, this.innValidRanges}) {
// Must provide one or the other
assert(innValidNums != null || innValidRanges != null);
// Do not provide empty list of valid nums
assert(innValidNums == null || innValidNums!.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,
innValidNums: [34, 37],
),
CardProvider(
id: CardProviderID.dinersClub,
cardLength: 16,
cvcLength: 3,
innValidNums: [30, 36, 38, 39],
),
CardProvider(
id: CardProviderID.discoverCard,
cardLength: 16,
cvcLength: 3,
innValidNums: [60, 65],
innValidRanges: [Range(low: 644, high: 649)],
),
CardProvider(
id: CardProviderID.jcb,
cardLength: 16,
cvcLength: 3,
innValidNums: [35],
),
CardProvider(
id: CardProviderID.mastercard,
cardLength: 16,
cvcLength: 3,
innValidRanges: [Range(low: 22, high: 27), Range(low: 51, high: 55)],
),
CardProvider(
id: CardProviderID.visa,
cardLength: 16,
cvcLength: 3,
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; int sum = 0;
bool isSecond = false; bool isSecond = false;
for (int i = digits.length - 1; i >= 0; i--) { for (int i = digits.length - 1; i >= 0; i--) {
@ -306,3 +234,118 @@ bool luhnAlgorithmCheck(List<int> digits) {
} }
return (sum % 10) == 0; return (sum % 10) == 0;
} }
}
/// Enum of validation states a `CardDetails` object can have.
enum ValidState {
ok,
error,
blank,
missingCard,
invalidCard,
missingDate,
invalidMonth,
dateTooEarly,
dateTooLate,
missingCVC,
invalidCVC,
missingZip,
invalidZip,
}
/// Enum of supported U.S. Card Providers
enum CardProviderID {
americanExpress,
dinersClub,
discoverCard,
mastercard,
jcb,
visa,
}
/// Encapsulates criteria for Card Providers in the U.S.
/// Used by `CardDetails.detectCardProvider()` to determine
/// a card's Provider.
class CardProvider {
CardProviderID id;
List<int>? innValidNums;
List<_Range>? innValidRanges;
int cardLength;
int cvcLength;
CardProvider(
{required this.id, required this.cardLength, required this.cvcLength, this.innValidNums, this.innValidRanges}) {
// Must provide one or the other
assert(innValidNums != null || innValidRanges != null);
// Do not provide empty list of valid nums
assert(innValidNums == null || innValidNums!.isNotEmpty);
}
@override
String toString() {
return id.toString();
}
}
/// 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 low;
_Range({required this.low, required this.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) {
return low <= val && val <= high;
}
}
/// List of CardProviders for US-based Credit / Debit Cards.
List<CardProvider> _providers = [
CardProvider(
id: CardProviderID.americanExpress,
cardLength: 15,
cvcLength: 4,
innValidNums: [34, 37],
),
CardProvider(
id: CardProviderID.dinersClub,
cardLength: 16,
cvcLength: 3,
innValidNums: [30, 36, 38, 39],
),
CardProvider(
id: CardProviderID.discoverCard,
cardLength: 16,
cvcLength: 3,
innValidNums: [60, 65],
innValidRanges: [_Range(low: 644, high: 649)],
),
CardProvider(
id: CardProviderID.jcb,
cardLength: 16,
cvcLength: 3,
innValidNums: [35],
),
CardProvider(
id: CardProviderID.mastercard,
cardLength: 16,
cvcLength: 3,
innValidRanges: [_Range(low: 22, high: 27), _Range(low: 51, high: 55)],
),
CardProvider(
id: CardProviderID.visa,
cardLength: 16,
cvcLength: 3,
innValidNums: [4],
)
];

View File

@ -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'),

View File

@ -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,
@ -23,6 +33,7 @@ class CardTextField extends StatefulWidget {
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();