From 0454ba6a340af8db94c634e785fd58129b554e3d Mon Sep 17 00:00:00 2001 From: Aditya Chavda Date: Thu, 21 Sep 2023 15:35:28 +0530 Subject: [PATCH] :art: Package Structure and Code Improvement - Fixed the package structure to not expose all files under lib folder to end users. - Created appropriate files for constants, asset paths, enumerations, etc. - Removed raw string from codebase and moved to constants.dart. - Moved text field validators to validators.dart. - Utilised typedefs instead of raw function definitions in the codebase. --- CHANGELOG.md | 1 + analysis_options.yaml | 1 - example/lib/main.dart | 1 - lib/constants.dart | 30 - lib/extension.dart | 5 - lib/flutter_credit_card.dart | 14 +- lib/{ => src}/credit_card_background.dart | 62 +- lib/{ => src}/credit_card_form.dart | 313 +++++----- lib/{ => src}/credit_card_widget.dart | 533 ++++-------------- .../flip_animation_builder.dart} | 11 +- lib/src/float_animation_builder.dart | 37 ++ .../floating_animation/cursor_listener.dart | 4 +- .../floating_animation/floating_config.dart | 2 +- .../floating_controller.dart | 2 +- .../floating_animation/floating_event.dart | 3 +- .../glare_effect_widget.dart | 2 +- lib/src/masked_text_controller.dart | 123 ++++ lib/{ => src/models}/credit_card_brand.dart | 2 +- lib/{ => src/models}/credit_card_model.dart | 0 .../models}/custom_card_type_icon.dart | 5 +- .../models}/glassmorphism_config.dart | 0 .../flutter_credit_card_method_channel.dart | 5 +- ...lutter_credit_card_platform_interface.dart | 2 +- .../plugin}/flutter_credit_card_web.dart | 2 +- lib/src/utils/asset_paths.dart | 13 + lib/src/utils/constants.dart | 139 +++++ lib/src/utils/enumerations.dart | 21 + lib/src/utils/extensions.dart | 13 + lib/src/utils/helpers.dart | 88 +++ lib/src/utils/typedefs.dart | 13 + lib/src/utils/validators.dart | 41 ++ 31 files changed, 789 insertions(+), 699 deletions(-) delete mode 100644 lib/constants.dart delete mode 100644 lib/extension.dart rename lib/{ => src}/credit_card_background.dart (76%) rename lib/{ => src}/credit_card_form.dart (62%) rename lib/{ => src}/credit_card_widget.dart (63%) rename lib/{credit_card_animation.dart => src/flip_animation_builder.dart} (66%) create mode 100644 lib/src/float_animation_builder.dart rename lib/{ => src}/floating_animation/cursor_listener.dart (98%) rename lib/{ => src}/floating_animation/floating_config.dart (98%) rename lib/{ => src}/floating_animation/floating_controller.dart (97%) rename lib/{ => src}/floating_animation/floating_event.dart (91%) rename lib/{ => src}/floating_animation/glare_effect_widget.dart (97%) create mode 100644 lib/src/masked_text_controller.dart rename lib/{ => src/models}/credit_card_brand.dart (57%) rename lib/{ => src/models}/credit_card_model.dart (100%) rename lib/{ => src/models}/custom_card_type_icon.dart (78%) rename lib/{ => src/models}/glassmorphism_config.dart (100%) rename lib/{ => src/plugin}/flutter_credit_card_method_channel.dart (94%) rename lib/{ => src/plugin}/flutter_credit_card_platform_interface.dart (96%) rename lib/{ => src/plugin}/flutter_credit_card_web.dart (93%) create mode 100644 lib/src/utils/asset_paths.dart create mode 100644 lib/src/utils/constants.dart create mode 100644 lib/src/utils/enumerations.dart create mode 100644 lib/src/utils/extensions.dart create mode 100644 lib/src/utils/helpers.dart create mode 100644 lib/src/utils/typedefs.dart create mode 100644 lib/src/utils/validators.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9657b..56ee7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed [#138](https://github.com/SimformSolutionsPvtLtd/flutter_credit_card/issues/138) AutoValidateMode only applied to Card Number text field. - Added dart 3 support [#146](https://github.com/SimformSolutionsPvtLtd/flutter_credit_card/pull/146). +- Fixed package structure and improved code overall [#150](https://github.com/SimformSolutionsPvtLtd/flutter_credit_card/pull/150). # [3.0.7](https://github.com/SimformSolutionsPvtLtd/flutter_credit_card/tree/3.0.7) diff --git a/analysis_options.yaml b/analysis_options.yaml index 00b6d48..0de69bc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -121,7 +121,6 @@ linter: - prefer_const_literals_to_create_immutables # - prefer_constructors_over_static_methods # not yet tested - prefer_contains - - prefer_equal_for_default_values # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods - prefer_final_fields - prefer_final_locals diff --git a/example/lib/main.dart b/example/lib/main.dart index ef72d0d..1bbdb86 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,6 @@ import 'package:example/app_colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_credit_card/credit_card_brand.dart'; import 'package:flutter_credit_card/flutter_credit_card.dart'; void main() => runApp(const MySample()); diff --git a/lib/constants.dart b/lib/constants.dart deleted file mode 100644 index 7eeb22b..0000000 --- a/lib/constants.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:math'; -import 'dart:ui'; - -class AppConstants { - static const double floatWebBreakPoint = 650; - static const double creditCardAspectRatio = 0.5714; - static const double creditCardPadding = 16; - - static const double minRestBackVel = 0.01; - static const double maxRestBackVel = 0.05; - static const double defaultRestBackVel = 0.8; - - static const Duration fps60 = Duration(microseconds: 16666); - static const Duration fps60Offset = Duration(microseconds: 16667); - - /// Color constants - static const Color defaultGlareColor = Color(0xffFFFFFF); - static const Color floatingShadowColor = Color(0x4D000000); - - static const double defaultMaximumAngle = pi / 10; - static const double minBlurRadius = 10; - - /// Gyroscope channel constants - static const String gyroMethodChannelName = 'com.simform.flutter_credit_card'; - static const String gyroEventChannelName = - 'com.simform.flutter_credit_card/gyroscope'; - static const String isGyroAvailableMethod = 'isGyroscopeAvailable'; - static const String initiateMethod = 'initiateEvents'; - static const String cancelMethod = 'cancelEvents'; -} diff --git a/lib/extension.dart b/lib/extension.dart deleted file mode 100644 index e8ac7f9..0000000 --- a/lib/extension.dart +++ /dev/null @@ -1,5 +0,0 @@ -extension NullableStringExtension on String? { - bool get isNullOrEmpty => this == null || (this?.isEmpty ?? false); - - bool get isNotNullAndNotEmpty => this != null && (this?.isNotEmpty ?? false); -} diff --git a/lib/flutter_credit_card.dart b/lib/flutter_credit_card.dart index 4d0c948..aa7b62b 100644 --- a/lib/flutter_credit_card.dart +++ b/lib/flutter_credit_card.dart @@ -1,8 +1,10 @@ library flutter_credit_card; -export 'credit_card_form.dart'; -export 'credit_card_model.dart'; -export 'credit_card_widget.dart'; -export 'custom_card_type_icon.dart'; -export 'floating_animation/floating_config.dart'; -export 'glassmorphism_config.dart'; +export 'src/credit_card_form.dart'; +export 'src/credit_card_widget.dart'; +export 'src/floating_animation/floating_config.dart'; +export 'src/models/credit_card_brand.dart'; +export 'src/models/credit_card_model.dart'; +export 'src/models/custom_card_type_icon.dart'; +export 'src/models/glassmorphism_config.dart'; +export 'src/utils/enumerations.dart' show CardType; diff --git a/lib/credit_card_background.dart b/lib/src/credit_card_background.dart similarity index 76% rename from lib/credit_card_background.dart rename to lib/src/credit_card_background.dart index ad45015..5f78fd1 100644 --- a/lib/credit_card_background.dart +++ b/lib/src/credit_card_background.dart @@ -1,12 +1,13 @@ -import 'dart:ui' as ui; +import 'dart:ui'; import 'package:flutter/material.dart'; -import 'constants.dart'; import 'floating_animation/floating_config.dart'; import 'floating_animation/floating_controller.dart'; import 'floating_animation/glare_effect_widget.dart'; -import 'glassmorphism_config.dart'; +import 'models/glassmorphism_config.dart'; +import 'utils/constants.dart'; +import 'utils/extensions.dart'; class CardBackground extends StatelessWidget { const CardBackground({ @@ -24,9 +25,7 @@ class CardBackground extends StatelessWidget { this.shadowConfig, super.key, }) : assert( - (backgroundImage == null && backgroundNetworkImage == null) || - (backgroundImage == null && backgroundNetworkImage != null) || - (backgroundImage != null && backgroundNetworkImage == null), + backgroundImage == null || backgroundNetworkImage == null, 'You can\'t use network image & asset image at same time as card' ' background', ); @@ -54,14 +53,20 @@ class CardBackground extends StatelessWidget { final double screenWidth = constraints.maxWidth.isInfinite ? screenSize.width : constraints.maxWidth; - final double screenHeight = screenSize.height; + final double implicitHeight = orientation.isPortrait + ? ((width ?? screenWidth) - (padding * 2)) * + AppConstants.creditCardAspectRatio + : screenSize.height / 2; return Stack( alignment: Alignment.center, children: [ Container( margin: EdgeInsets.all(padding), + width: width ?? screenWidth, + height: height ?? implicitHeight, + clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: AppConstants.creditCardBorderRadius, boxShadow: shadowConfig != null && floatingController != null ? [ BoxShadow( @@ -90,39 +95,26 @@ class CardBackground extends StatelessWidget { ) : null, ), - width: width ?? screenWidth, - height: height ?? - (orientation == Orientation.portrait - ? (((width ?? screenWidth) - (padding * 2)) * - AppConstants.creditCardAspectRatio) - : screenHeight / 2), - child: ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(8), - child: GlareEffectWidget( - border: border, - glarePosition: glarePosition, - child: glassmorphismConfig == null - ? child - : BackdropFilter( - filter: ui.ImageFilter.blur( - sigmaX: glassmorphismConfig!.blurX, - sigmaY: glassmorphismConfig!.blurY, - ), - child: child, + child: GlareEffectWidget( + border: border, + glarePosition: glarePosition, + child: glassmorphismConfig == null + ? child + : BackdropFilter( + filter: ImageFilter.blur( + sigmaX: glassmorphismConfig!.blurX, + sigmaY: glassmorphismConfig!.blurY, ), - ), + child: child, + ), ), ), if (glassmorphismConfig != null) Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(padding), child: _GlassmorphicBorder( width: width ?? screenWidth, - height: height ?? - (orientation == Orientation.portrait - ? ((screenWidth - 32) * 0.5714) - : screenHeight / 2), + height: height ?? implicitHeight, ), ), ], @@ -148,7 +140,7 @@ class _GlassmorphicBorder extends StatelessWidget { size: MediaQuery.of(context).size, child: Container( decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10)), + borderRadius: AppConstants.creditCardBorderRadius, ), width: width, height: height, diff --git a/lib/credit_card_form.dart b/lib/src/credit_card_form.dart similarity index 62% rename from lib/credit_card_form.dart rename to lib/src/credit_card_form.dart index 16bdf25..8b6bf6c 100644 --- a/lib/credit_card_form.dart +++ b/lib/src/credit_card_form.dart @@ -1,42 +1,46 @@ import 'package:flutter/material.dart'; -import 'package:flutter_credit_card/flutter_credit_card.dart'; + +import '../flutter_credit_card.dart'; +import 'masked_text_controller.dart'; +import 'utils/constants.dart'; +import 'utils/typedefs.dart'; +import 'utils/validators.dart'; class CreditCardForm extends StatefulWidget { const CreditCardForm({ - Key? key, required this.cardNumber, required this.expiryDate, required this.cardHolderName, required this.cvvCode, - this.obscureCvv = false, - this.obscureNumber = false, required this.onCreditCardModelChange, required this.themeColor, + required this.formKey, + this.obscureCvv = false, + this.obscureNumber = false, this.textColor = Colors.black, this.cursorColor, this.cardHolderDecoration = const InputDecoration( - labelText: 'Card holder', + labelText: AppConstants.cardHolder, ), this.cardNumberDecoration = const InputDecoration( - labelText: 'Card number', - hintText: 'XXXX XXXX XXXX XXXX', + labelText: AppConstants.cardNumber, + hintText: AppConstants.sixteenX, ), this.expiryDateDecoration = const InputDecoration( - labelText: 'Expired Date', - hintText: 'MM/YY', + labelText: AppConstants.expiryDate, + hintText: AppConstants.expiryDateShort, ), this.cvvCodeDecoration = const InputDecoration( - labelText: 'CVV', - hintText: 'XXX', + labelText: AppConstants.cvv, + hintText: AppConstants.threeX, ), - required this.formKey, this.cardNumberKey, this.cardHolderKey, this.expiryDateKey, this.cvvCodeKey, - this.cvvValidationMessage = 'Please input a valid CVV', - this.dateValidationMessage = 'Please input a valid date', - this.numberValidationMessage = 'Please input a valid number', + this.cvvValidationMessage = AppConstants.cvvValidationMessage, + this.dateValidationMessage = AppConstants.dateValidationMessage, + this.numberValidationMessage = AppConstants.numberValidationMessage, this.isHolderNameVisible = true, this.isCardNumberVisible = true, this.isExpiryDateVisible = true, @@ -48,7 +52,8 @@ class CreditCardForm extends StatefulWidget { this.cardHolderValidator, this.onFormComplete, this.disableCardNumberAutoFillHints = false, - }) : super(key: key); + super.key, + }); /// A string indicating card number in the text field. final String cardNumber; @@ -72,7 +77,7 @@ class CreditCardForm extends StatefulWidget { final String numberValidationMessage; /// Provides callback when there is any change in [CreditCardModel]. - final void Function(CreditCardModel) onCreditCardModelChange; + final CCModelChangeCallback onCreditCardModelChange; /// Color of the theme of the credit card form. final Color themeColor; @@ -142,16 +147,16 @@ class CreditCardForm extends StatefulWidget { final AutovalidateMode? autovalidateMode; /// A validator for card number text field. - final String? Function(String?)? cardNumberValidator; + final ValidationCallback? cardNumberValidator; /// A validator for expiry date text field. - final String? Function(String?)? expiryDateValidator; + final ValidationCallback? expiryDateValidator; /// A validator for cvv code text field. - final String? Function(String?)? cvvValidator; + final ValidationCallback? cvvValidator; /// A validator for card holder text field. - final String? Function(String?)? cardHolderValidator; + final ValidationCallback? cardHolderValidator; /// Setting this flag to true will disable autofill hints for Credit card /// number text field. Flutter has a bug when auto fill hints are enabled for @@ -176,61 +181,41 @@ class _CreditCardFormState extends State { bool isCvvFocused = false; late Color themeColor; - late void Function(CreditCardModel) onCreditCardModelChange; - late CreditCardModel creditCardModel; + late final CreditCardModel creditCardModel; + late final CCModelChangeCallback onCreditCardModelChange = + widget.onCreditCardModelChange; - final MaskedTextController _cardNumberController = - MaskedTextController(mask: '0000 0000 0000 0000'); - final TextEditingController _expiryDateController = - MaskedTextController(mask: '00/00'); - final TextEditingController _cardHolderNameController = - TextEditingController(); - final TextEditingController _cvvCodeController = - MaskedTextController(mask: '0000'); + late final MaskedTextController _cardNumberController = MaskedTextController( + mask: AppConstants.cardNumberMask, + text: widget.cardNumber, + ); - FocusNode cvvFocusNode = FocusNode(); - FocusNode expiryDateNode = FocusNode(); - FocusNode cardHolderNode = FocusNode(); + late final TextEditingController _expiryDateController = MaskedTextController( + mask: AppConstants.expiryDateMask, + text: widget.expiryDate, + ); - void textFieldFocusDidChange() { - creditCardModel.isCvvFocused = cvvFocusNode.hasFocus; - onCreditCardModelChange(creditCardModel); - } + late final TextEditingController _cardHolderNameController = + TextEditingController( + text: widget.cardHolderName, + ); - void createCreditCardModel() { - cardNumber = widget.cardNumber; - expiryDate = widget.expiryDate; - cardHolderName = widget.cardHolderName; - cvvCode = widget.cvvCode; + late final TextEditingController _cvvCodeController = MaskedTextController( + mask: AppConstants.cvvMask, + text: widget.cvvCode, + ); - creditCardModel = CreditCardModel( - cardNumber, expiryDate, cardHolderName, cvvCode, isCvvFocused); - } + final FocusNode cvvFocusNode = FocusNode(); + final FocusNode expiryDateNode = FocusNode(); + final FocusNode cardHolderNode = FocusNode(); @override void initState() { super.initState(); - createCreditCardModel(); - - _cardNumberController.text = widget.cardNumber; - _expiryDateController.text = widget.expiryDate; - _cardHolderNameController.text = widget.cardHolderName; - _cvvCodeController.text = widget.cvvCode; - - onCreditCardModelChange = widget.onCreditCardModelChange; - cvvFocusNode.addListener(textFieldFocusDidChange); } - @override - void dispose() { - cardHolderNode.dispose(); - cvvFocusNode.dispose(); - expiryDateNode.dispose(); - super.dispose(); - } - @override void didChangeDependencies() { themeColor = widget.themeColor; @@ -257,20 +242,11 @@ class _CreditCardFormState extends State { key: widget.cardNumberKey, obscureText: widget.obscureNumber, controller: _cardNumberController, - onChanged: (String value) { - setState(() { - cardNumber = _cardNumberController.text; - creditCardModel.cardNumber = cardNumber; - onCreditCardModelChange(creditCardModel); - }); - }, + onChanged: _onCardNumberChange, cursorColor: widget.cursorColor ?? themeColor, - onEditingComplete: () { - FocusScope.of(context).requestFocus(expiryDateNode); - }, - style: TextStyle( - color: widget.textColor, - ), + onEditingComplete: () => + FocusScope.of(context).requestFocus(expiryDateNode), + style: TextStyle(color: widget.textColor), decoration: widget.cardNumberDecoration, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, @@ -279,13 +255,10 @@ class _CreditCardFormState extends State { : const [AutofillHints.creditCardNumber], autovalidateMode: widget.autovalidateMode, validator: widget.cardNumberValidator ?? - (String? value) { - // Validate less that 13 digits +3 white spaces - if (value!.isEmpty || value.length < 16) { - return widget.numberValidationMessage; - } - return null; - }, + (String? value) => Validators.cardNumberValidator( + value, + widget.numberValidationMessage, + ), ), ), ), @@ -301,26 +274,12 @@ class _CreditCardFormState extends State { child: TextFormField( key: widget.expiryDateKey, controller: _expiryDateController, - onChanged: (String value) { - if (_expiryDateController.text - .startsWith(RegExp('[2-9]'))) { - _expiryDateController.text = - '0${_expiryDateController.text}'; - } - setState(() { - expiryDate = _expiryDateController.text; - creditCardModel.expiryDate = expiryDate; - onCreditCardModelChange(creditCardModel); - }); - }, + onChanged: _onExpiryDateChange, cursorColor: widget.cursorColor ?? themeColor, focusNode: expiryDateNode, - onEditingComplete: () { - FocusScope.of(context).requestFocus(cvvFocusNode); - }, - style: TextStyle( - color: widget.textColor, - ), + onEditingComplete: () => + FocusScope.of(context).requestFocus(cvvFocusNode), + style: TextStyle(color: widget.textColor), decoration: widget.expiryDateDecoration, autovalidateMode: widget.autovalidateMode, keyboardType: TextInputType.number, @@ -329,28 +288,10 @@ class _CreditCardFormState extends State { AutofillHints.creditCardExpirationDate ], validator: widget.expiryDateValidator ?? - (String? value) { - if (value!.isEmpty) { - return widget.dateValidationMessage; - } - final DateTime now = DateTime.now(); - final List date = - value.split(RegExp(r'/')); - final int month = int.parse(date.first); - final int year = int.parse('20${date.last}'); - final int lastDayOfMonth = month < 12 - ? DateTime(year, month + 1, 0).day - : DateTime(year + 1, 1, 0).day; - final DateTime cardDate = DateTime( - year, month, lastDayOfMonth, 23, 59, 59, 999); - - if (cardDate.isBefore(now) || - month > 12 || - month == 0) { - return widget.dateValidationMessage; - } - return null; - }, + (String? value) => Validators.expiryDateValidator( + value, + widget.dateValidationMessage, + ), ), ), ), @@ -368,18 +309,8 @@ class _CreditCardFormState extends State { focusNode: cvvFocusNode, controller: _cvvCodeController, cursorColor: widget.cursorColor ?? themeColor, - onEditingComplete: () { - if (widget.isHolderNameVisible) { - FocusScope.of(context).requestFocus(cardHolderNode); - } else { - FocusScope.of(context).unfocus(); - onCreditCardModelChange(creditCardModel); - widget.onFormComplete?.call(); - } - }, - style: TextStyle( - color: widget.textColor, - ), + onEditingComplete: _onCvvEditComplete, + style: TextStyle(color: widget.textColor), decoration: widget.cvvCodeDecoration, keyboardType: TextInputType.number, autovalidateMode: widget.autovalidateMode, @@ -389,20 +320,12 @@ class _CreditCardFormState extends State { autofillHints: const [ AutofillHints.creditCardSecurityCode ], - onChanged: (String text) { - setState(() { - cvvCode = text; - creditCardModel.cvvCode = cvvCode; - onCreditCardModelChange(creditCardModel); - }); - }, + onChanged: _onCvvChange, validator: widget.cvvValidator ?? - (String? value) { - if (value!.isEmpty || value.length < 3) { - return widget.cvvValidationMessage; - } - return null; - }, + (String? value) => Validators.cvvValidator( + value, + widget.cvvValidationMessage, + ), ), ), ), @@ -417,28 +340,16 @@ class _CreditCardFormState extends State { child: TextFormField( key: widget.cardHolderKey, controller: _cardHolderNameController, - onChanged: (String value) { - setState(() { - cardHolderName = _cardHolderNameController.text; - creditCardModel.cardHolderName = cardHolderName; - onCreditCardModelChange(creditCardModel); - }); - }, + onChanged: _onCardHolderNameChange, cursorColor: widget.cursorColor ?? themeColor, focusNode: cardHolderNode, - style: TextStyle( - color: widget.textColor, - ), + style: TextStyle(color: widget.textColor), decoration: widget.cardHolderDecoration, keyboardType: TextInputType.text, autovalidateMode: widget.autovalidateMode, textInputAction: TextInputAction.done, autofillHints: const [AutofillHints.creditCardName], - onEditingComplete: () { - FocusScope.of(context).unfocus(); - onCreditCardModelChange(creditCardModel); - widget.onFormComplete?.call(); - }, + onEditingComplete: _onHolderNameEditComplete, validator: widget.cardHolderValidator, ), ), @@ -448,4 +359,80 @@ class _CreditCardFormState extends State { ), ); } + + @override + void dispose() { + cardHolderNode.dispose(); + cvvFocusNode.dispose(); + expiryDateNode.dispose(); + super.dispose(); + } + + void textFieldFocusDidChange() { + isCvvFocused = creditCardModel.isCvvFocused = cvvFocusNode.hasFocus; + onCreditCardModelChange(creditCardModel); + } + + void createCreditCardModel() { + cardNumber = widget.cardNumber; + expiryDate = widget.expiryDate; + cardHolderName = widget.cardHolderName; + cvvCode = widget.cvvCode; + + creditCardModel = CreditCardModel( + cardNumber, + expiryDate, + cardHolderName, + cvvCode, + isCvvFocused, + ); + } + + void _onCardNumberChange(String value) { + setState(() { + creditCardModel.cardNumber = cardNumber = _cardNumberController.text; + onCreditCardModelChange(creditCardModel); + }); + } + + void _onExpiryDateChange(String value) { + final String expiry = _expiryDateController.text; + _expiryDateController.text = + expiry.startsWith(RegExp('[2-9]')) ? '0$expiry' : expiry; + setState(() { + creditCardModel.expiryDate = expiryDate = expiry; + onCreditCardModelChange(creditCardModel); + }); + } + + void _onCvvChange(String text) { + setState(() { + creditCardModel.cvvCode = cvvCode = text; + onCreditCardModelChange(creditCardModel); + }); + } + + void _onCardHolderNameChange(String value) { + setState(() { + creditCardModel.cardHolderName = + cardHolderName = _cardHolderNameController.text; + onCreditCardModelChange(creditCardModel); + }); + } + + void _onCvvEditComplete() { + if (widget.isHolderNameVisible) { + FocusScope.of(context).requestFocus(cardHolderNode); + } else { + FocusScope.of(context).unfocus(); + onCreditCardModelChange(creditCardModel); + widget.onFormComplete?.call(); + } + } + + void _onHolderNameEditComplete() { + FocusScope.of(context).unfocus(); + onCreditCardModelChange(creditCardModel); + widget.onFormComplete?.call(); + } } diff --git a/lib/credit_card_widget.dart b/lib/src/credit_card_widget.dart similarity index 63% rename from lib/credit_card_widget.dart rename to lib/src/credit_card_widget.dart index 78ede0b..ed15655 100644 --- a/lib/credit_card_widget.dart +++ b/lib/src/credit_card_widget.dart @@ -3,29 +3,23 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'constants.dart'; -import 'credit_card_animation.dart'; import 'credit_card_background.dart'; -import 'credit_card_brand.dart'; -import 'custom_card_type_icon.dart'; -import 'extension.dart'; +import 'flip_animation_builder.dart'; +import 'float_animation_builder.dart'; import 'floating_animation/cursor_listener.dart'; import 'floating_animation/floating_config.dart'; import 'floating_animation/floating_controller.dart'; import 'floating_animation/floating_event.dart'; -import 'flutter_credit_card_platform_interface.dart'; -import 'glassmorphism_config.dart'; - -const Map cardTypeIconAsset = { - CardType.visa: 'icons/visa.png', - CardType.rupay: 'icons/rupay.png', - CardType.americanExpress: 'icons/amex.png', - CardType.mastercard: 'icons/mastercard.png', - CardType.unionpay: 'icons/unionpay.png', - CardType.discover: 'icons/discover.png', - CardType.elo: 'icons/elo.png', - CardType.hipercard: 'icons/hipercard.png', -}; +import 'models/credit_card_brand.dart'; +import 'models/custom_card_type_icon.dart'; +import 'models/glassmorphism_config.dart'; +import 'plugin/flutter_credit_card_platform_interface.dart'; +import 'utils/asset_paths.dart'; +import 'utils/constants.dart'; +import 'utils/enumerations.dart'; +import 'utils/extensions.dart'; +import 'utils/helpers.dart'; +import 'utils/typedefs.dart'; class CreditCardWidget extends StatefulWidget { /// A widget showcasing credit card UI. @@ -37,16 +31,16 @@ class CreditCardWidget extends StatefulWidget { required this.showBackView, required this.onCreditCardWidgetChange, this.bankName, - this.animationDuration = const Duration(milliseconds: 500), + this.animationDuration = AppConstants.defaultAnimDuration, this.height, this.width, this.textStyle, - this.cardBgColor = const Color(0xff1b447b), + this.cardBgColor = AppConstants.defaultCardBgColor, this.obscureCardNumber = true, this.obscureCardCvv = true, - this.labelCardHolder = 'CARD HOLDER', - this.labelExpiredDate = 'MM/YY', - this.labelValidThru = 'VALID\nTHRU', + this.labelCardHolder = AppConstants.cardHolderCaps, + this.labelExpiredDate = AppConstants.expiryDateShort, + this.labelValidThru = AppConstants.validThru, this.cardType, this.isHolderNameVisible = false, this.backgroundImage, @@ -113,7 +107,7 @@ class CreditCardWidget extends StatefulWidget { final bool obscureCardCvv; /// Provides a callback any time there is a change in credit card brand. - final void Function(CreditCardBrand) onCreditCardWidgetChange; + final CCBrandChangeCallback onCreditCardWidgetChange; /// Enable/disable card holder name. Defaults to false. final bool isHolderNameVisible; @@ -266,6 +260,7 @@ class _CreditCardWidgetState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { + // TODO(aditya): Use AppLifecycleListener once Flutter 3.13 is the minimum support version. switch (state) { case AppLifecycleState.inactive: _handleFloatingAnimationSetup(shouldCancel: true); @@ -279,32 +274,6 @@ class _CreditCardWidgetState extends State super.didChangeAppLifecycleState(state); } - void _gradientSetup() { - backgroundGradientColor = LinearGradient( - // Where the linear gradient begins and ends - begin: Alignment.topRight, - end: Alignment.bottomLeft, - // Add one stop for each color. Stops should increase from 0 to 1 - stops: const [0.1, 0.4, 0.7, 0.9], - colors: [ - widget.cardBgColor.withOpacity(1), - widget.cardBgColor.withOpacity(0.97), - widget.cardBgColor.withOpacity(0.90), - widget.cardBgColor.withOpacity(0.86), - ], - ); - } - - @override - void dispose() { - FlutterCreditCardPlatform.instance.dispose(); - controller.dispose(); - backCardFloatStream.close(); - frontCardFloatStream.close(); - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - @override Widget build(BuildContext context) { /// @@ -326,13 +295,13 @@ class _CreditCardWidgetState extends State return Stack( children: [ _cardGesture( - child: AnimationCard( + child: FlipAnimationBuilder( animation: _frontRotation, child: _buildFrontContainer(), ), ), _cardGesture( - child: AnimationCard( + child: FlipAnimationBuilder( animation: _backRotation, child: _buildBackContainer(), ), @@ -363,6 +332,32 @@ class _CreditCardWidgetState extends State ); } + @override + void dispose() { + FlutterCreditCardPlatform.instance.dispose(); + controller.dispose(); + backCardFloatStream.close(); + frontCardFloatStream.close(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + void _gradientSetup() { + backgroundGradientColor = LinearGradient( + // Where the linear gradient begins and ends + begin: Alignment.topRight, + end: Alignment.bottomLeft, + // Add one stop for each color. Stops should increase from 0 to 1 + stops: const [0.1, 0.4, 0.7, 0.9], + colors: [ + widget.cardBgColor.withOpacity(1), + widget.cardBgColor.withOpacity(0.97), + widget.cardBgColor.withOpacity(0.90), + widget.cardBgColor.withOpacity(0.86), + ], + ); + } + void _processFloatingEvent(FloatingEvent? event) { if (event == null || controller.isAnimating) { return; @@ -433,15 +428,15 @@ class _CreditCardWidgetState extends State Theme.of(context).textTheme.titleLarge!.merge( const TextStyle( color: Colors.white, - fontFamily: 'halter', + fontFamily: AppConstants.fontFamily, fontSize: 15, - package: 'flutter_credit_card', + package: AppConstants.packageName, ), ); String number = widget.cardNumber; if (widget.obscureCardNumber) { - final String stripped = number.replaceAll(RegExp(r'\D'), ''); + final String stripped = number.replaceAll(RegExp(r'[^\d]'), ''); if (widget.obscureInitialCardNumber && stripped.length > 4) { final String start = number .substring(0, number.length - 5) @@ -458,29 +453,16 @@ class _CreditCardWidgetState extends State } } - if (widget.enableFloatingCard && isFrontVisible) { - return StreamBuilder( - stream: frontCardFloatStream.stream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - return Transform( - transform: floatController.transform( - snapshot.data, - shouldAvoid: controller.isAnimating, - ), - alignment: FractionalOffset.center, - child: _frontCardBackground( - defaultTextStyle: defaultTextStyle, - number: number, - ), - ); - }, - ); - } else { - return _frontCardBackground( + return FloatAnimationBuilder( + isEnabled: widget.enableFloatingCard && isFrontVisible, + stream: frontCardFloatStream.stream, + onEvent: (FloatingEvent? event) => + floatController.transform(event, shouldAvoid: controller.isAnimating), + child: _frontCardBackground( defaultTextStyle: defaultTextStyle, number: number, - ); - } + ), + ); } Widget _frontCardBackground({ @@ -524,8 +506,8 @@ class _CreditCardWidgetState extends State Padding( padding: const EdgeInsetsDirectional.only(start: 16), child: Image.asset( - 'icons/chip.png', - package: 'flutter_credit_card', + AssetPaths.chip, + package: AppConstants.packageName, color: widget.chipColor, scale: 1, ), @@ -538,7 +520,7 @@ class _CreditCardWidgetState extends State child: Padding( padding: const EdgeInsetsDirectional.only(start: 16), child: Text( - widget.cardNumber.isEmpty ? 'XXXX XXXX XXXX XXXX' : number, + widget.cardNumber.isEmpty ? AppConstants.sixteenX : number, style: widget.textStyle ?? defaultTextStyle, ), ), @@ -587,9 +569,7 @@ class _CreditCardWidgetState extends State ), ), ), - widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), + _getCardTypeIcon(), ], ), ), @@ -606,9 +586,9 @@ class _CreditCardWidgetState extends State Theme.of(context).textTheme.titleLarge!.merge( const TextStyle( color: Colors.black, - fontFamily: 'halter', + fontFamily: AppConstants.fontFamily, fontSize: 16, - package: 'flutter_credit_card', + package: AppConstants.packageName, ), ); @@ -616,28 +596,13 @@ class _CreditCardWidgetState extends State ? widget.cvvCode.replaceAll(RegExp(r'\d'), '*') : widget.cvvCode; - return widget.enableFloatingCard && !isFrontVisible - ? StreamBuilder( - stream: backCardFloatStream.stream, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return Transform( - transform: floatController.transform( - snapshot.data, - shouldAvoid: controller.isAnimating, - ), - alignment: FractionalOffset.center, - child: _backCardBackground( - cvv: cvv, - defaultTextStyle: defaultTextStyle, - ), - ); - }, - ) - : _backCardBackground( - cvv: cvv, - defaultTextStyle: defaultTextStyle, - ); + return FloatAnimationBuilder( + isEnabled: widget.enableFloatingCard && !isFrontVisible, + stream: backCardFloatStream.stream, + onEvent: (FloatingEvent? event) => + floatController.transform(event, shouldAvoid: controller.isAnimating), + child: _backCardBackground(defaultTextStyle: defaultTextStyle, cvv: cvv), + ); } Widget _backCardBackground({ @@ -673,7 +638,6 @@ class _CreditCardWidgetState extends State child: Container( margin: const EdgeInsets.only(top: 16), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( flex: 9, @@ -691,8 +655,8 @@ class _CreditCardWidgetState extends State child: Text( widget.cvvCode.isEmpty ? isAmex - ? 'XXXX' - : 'XXX' + ? AppConstants.fourX + : AppConstants.threeX : cvv, maxLines: 1, style: widget.textStyle ?? defaultTextStyle, @@ -710,9 +674,7 @@ class _CreditCardWidgetState extends State alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), + child: _getCardTypeIcon(), ), ), ), @@ -723,24 +685,27 @@ class _CreditCardWidgetState extends State Widget _cardGesture({required Widget child}) { bool isRightSwipe = true; + double childHalfWidth = 0.0; return widget.isSwipeGestureEnabled ? GestureDetector( + onTapUp: (TapUpDetails details) { + isGestureUpdate = true; + _toggleSide( + flipFromRight: details.localPosition.dx > childHalfWidth, + ); + }, onPanEnd: (_) { isGestureUpdate = true; _toggleSide(flipFromRight: isRightSwipe); }, - onPanUpdate: (DragUpdateDetails details) { - // Swiping in right direction. - if (details.delta.dx > 0) { - isRightSwipe = true; - } - - // Swiping in left direction. - if (details.delta.dx < 0) { - isRightSwipe = false; - } - }, - child: child, + onPanUpdate: (DragUpdateDetails details) => + isRightSwipe = !details.delta.dx.isNegative, + child: LayoutBuilder( + builder: (_, BoxConstraints constraints) { + childHalfWidth = constraints.maxWidth / 2; + return child; + }, + ), ) : child; } @@ -763,320 +728,12 @@ class _CreditCardWidgetState extends State }); } - /// Credit Card prefix patterns as of March 2019 - /// A [List] represents a range. - /// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55' - Map>> cardNumPatterns = - >>{ - CardType.visa: >{ - ['4'], - }, - CardType.rupay: >{ - ['60'], - ['6521'], - ['6522'], - }, - CardType.americanExpress: >{ - ['34'], - ['37'], - }, - CardType.unionpay: >{ - ['62'], - }, - CardType.discover: >{ - ['6011'], - ['622126', '622925'], // China UnionPay co-branded - ['644', '649'], - ['65'] - }, - CardType.mastercard: >{ - ['51', '55'], - ['2221', '2229'], - ['223', '229'], - ['23', '26'], - ['270', '271'], - ['2720'], - }, - CardType.elo: >{ - ['401178'], - ['401179'], - ['438935'], - ['457631'], - ['457632'], - ['431274'], - ['451416'], - ['457393'], - ['504175'], - ['506699', '506778'], - ['509000', '509999'], - ['627780'], - ['636297'], - ['636368'], - ['650031', '650033'], - ['650035', '650051'], - ['650405', '650439'], - ['650485', '650538'], - ['650541', '650598'], - ['650700', '650718'], - ['650720', '650727'], - ['650901', '650978'], - ['651652', '651679'], - ['655000', '655019'], - ['655021', '655058'] - }, - CardType.hipercard: >{ - ['606282'], - }, - }; - - /// This function determines the Credit Card type based on the cardPatterns - /// and returns it. - CardType detectCCType(String cardNumber) { - //Default card type is other - CardType cardType = CardType.otherBrand; - - if (cardNumber.isEmpty) { - return cardType; - } - - cardNumPatterns.forEach( - (CardType type, Set> patterns) { - for (List patternRange in patterns) { - // Remove any spaces - String ccPatternStr = - cardNumber.replaceAll(RegExp(r'\s+\b|\b\s'), ''); - final int rangeLen = patternRange[0].length; - // Trim the Credit Card number string to match the pattern prefix length - if (rangeLen < cardNumber.length) { - ccPatternStr = ccPatternStr.substring(0, rangeLen); - } - - if (patternRange.length > 1) { - // Convert the prefix range into numbers then make sure the - // Credit Card num is in the pattern range. - // Because Strings don't have '>=' type operators - final int ccPrefixAsInt = int.parse(ccPatternStr); - final int startPatternPrefixAsInt = int.parse(patternRange[0]); - final int endPatternPrefixAsInt = int.parse(patternRange[1]); - if (ccPrefixAsInt >= startPatternPrefixAsInt && - ccPrefixAsInt <= endPatternPrefixAsInt) { - // Found a match - cardType = type; - break; - } - } else { - // Just compare the single pattern prefix with the Credit Card prefix - if (ccPatternStr == patternRange[0]) { - // Found a match - cardType = type; - break; - } - } - } - }, + Widget _getCardTypeIcon() { + final CardType ccType = widget.cardType ?? detectCCType(widget.cardNumber); + isAmex = ccType == CardType.americanExpress; + return getCardTypeImage( + cardType: ccType, + customIcons: widget.customCardTypeIcons, ); - - return cardType; - } - - Widget getCardTypeImage(CardType? cardType) { - final List customCardTypeIcon = - getCustomCardTypeIcon(cardType!); - if (customCardTypeIcon.isNotEmpty) { - return customCardTypeIcon.first.cardImage; - } else { - return Image.asset( - cardTypeIconAsset[cardType]!, - height: 48, - width: 48, - package: 'flutter_credit_card', - ); - } - } - -// This method returns the icon for the visa card type if found -// else will return the empty container - Widget getCardTypeIcon(String cardNumber) { - Widget icon; - final CardType ccType = detectCCType(cardNumber); - final List customCardTypeIcon = - getCustomCardTypeIcon(ccType); - if (customCardTypeIcon.isNotEmpty) { - icon = customCardTypeIcon.first.cardImage; - isAmex = ccType == CardType.americanExpress; - } else { - switch (ccType) { - case CardType.visa: - case CardType.rupay: - case CardType.unionpay: - case CardType.discover: - case CardType.mastercard: - case CardType.elo: - case CardType.hipercard: - icon = Image.asset( - cardTypeIconAsset[ccType]!, - height: 48, - width: 48, - package: 'flutter_credit_card', - ); - isAmex = false; - break; - - case CardType.americanExpress: - icon = Image.asset( - cardTypeIconAsset[ccType]!, - height: 48, - width: 48, - package: 'flutter_credit_card', - ); - isAmex = true; - break; - - default: - icon = const SizedBox(height: 48, width: 48); - isAmex = false; - break; - } - } - - return icon; - } - - List getCustomCardTypeIcon(CardType currentCardType) => - widget.customCardTypeIcons - .where((CustomCardTypeIcon element) => - element.cardType == currentCardType) - .toList(); -} - -class MaskedTextController extends TextEditingController { - MaskedTextController( - {String? text, required this.mask, Map? translator}) - : super(text: text) { - this.translator = translator ?? MaskedTextController.getDefaultTranslator(); - - addListener(() { - final String previous = _lastUpdatedText; - if (beforeChange(previous, this.text)) { - updateText(this.text); - afterChange(previous, this.text); - } else { - updateText(_lastUpdatedText); - } - }); - - updateText(this.text); } - - String mask; - - late Map translator; - - Function afterChange = (String previous, String next) {}; - Function beforeChange = (String previous, String next) { - return true; - }; - - String _lastUpdatedText = ''; - - void updateText(String text) { - if (text.isNotEmpty) { - this.text = _applyMask(mask, text); - } else { - this.text = ''; - } - - _lastUpdatedText = this.text; - } - - void updateMask(String mask, {bool moveCursorToEnd = true}) { - this.mask = mask; - updateText(text); - - if (moveCursorToEnd) { - this.moveCursorToEnd(); - } - } - - void moveCursorToEnd() { - final String text = _lastUpdatedText; - selection = TextSelection.fromPosition(TextPosition(offset: text.length)); - } - - @override - set text(String newText) { - if (super.text != newText) { - super.text = newText; - moveCursorToEnd(); - } - } - - static Map getDefaultTranslator() { - return { - 'A': RegExp(r'[A-Za-z]'), - '0': RegExp(r'[0-9]'), - '@': RegExp(r'[A-Za-z0-9]'), - '*': RegExp(r'.*') - }; - } - - String _applyMask(String? mask, String value) { - String result = ''; - - int maskCharIndex = 0; - int valueCharIndex = 0; - - while (true) { - // if mask is ended, break. - if (maskCharIndex == mask!.length) { - break; - } - - // if value is ended, break. - if (valueCharIndex == value.length) { - break; - } - - final String maskChar = mask[maskCharIndex]; - final String valueChar = value[valueCharIndex]; - - // value equals mask, just set - if (maskChar == valueChar) { - result += maskChar; - valueCharIndex += 1; - maskCharIndex += 1; - continue; - } - - // apply translator if match - if (translator.containsKey(maskChar)) { - if (translator[maskChar]!.hasMatch(valueChar)) { - result += valueChar; - maskCharIndex += 1; - } - - valueCharIndex += 1; - continue; - } - - // not masked value, fixed char on mask - result += maskChar; - maskCharIndex += 1; - continue; - } - - return result; - } -} - -enum CardType { - otherBrand, - mastercard, - visa, - rupay, - americanExpress, - unionpay, - discover, - elo, - hipercard, } diff --git a/lib/credit_card_animation.dart b/lib/src/flip_animation_builder.dart similarity index 66% rename from lib/credit_card_animation.dart rename to lib/src/flip_animation_builder.dart index 2588305..9f55961 100644 --- a/lib/credit_card_animation.dart +++ b/lib/src/flip_animation_builder.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class AnimationCard extends StatelessWidget { - const AnimationCard({ +class FlipAnimationBuilder extends StatelessWidget { + const FlipAnimationBuilder({ required this.child, required this.animation, super.key, @@ -15,11 +15,10 @@ class AnimationCard extends StatelessWidget { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { - final Matrix4 transform = Matrix4.identity(); - transform.setEntry(3, 2, 0.001); - transform.rotateY(animation.value); return Transform( - transform: transform, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(animation.value), alignment: Alignment.center, child: child, ); diff --git a/lib/src/float_animation_builder.dart b/lib/src/float_animation_builder.dart new file mode 100644 index 0000000..71f8055 --- /dev/null +++ b/lib/src/float_animation_builder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'floating_animation/floating_event.dart'; +import 'utils/typedefs.dart'; + +class FloatAnimationBuilder extends StatelessWidget { + const FloatAnimationBuilder({ + required this.isEnabled, + required this.stream, + required this.onEvent, + required this.child, + super.key, + }); + + final bool isEnabled; + final Stream stream; + final FloatEventCallback onEvent; + final Widget child; + + @override + Widget build(BuildContext context) { + return isEnabled + ? StreamBuilder( + stream: stream, + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) => + Transform( + transform: onEvent(snapshot.data), + alignment: FractionalOffset.center, + child: child, + ), + ) + : child; + } +} diff --git a/lib/floating_animation/cursor_listener.dart b/lib/src/floating_animation/cursor_listener.dart similarity index 98% rename from lib/floating_animation/cursor_listener.dart rename to lib/src/floating_animation/cursor_listener.dart index 17b1f76..9fec8a1 100644 --- a/lib/floating_animation/cursor_listener.dart +++ b/lib/src/floating_animation/cursor_listener.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import '../constants.dart'; +import '../utils/constants.dart'; +import '../utils/enumerations.dart'; import 'floating_event.dart'; class CursorListener extends StatefulWidget { diff --git a/lib/floating_animation/floating_config.dart b/lib/src/floating_animation/floating_config.dart similarity index 98% rename from lib/floating_animation/floating_config.dart rename to lib/src/floating_animation/floating_config.dart index 5e499f0..9a570b9 100644 --- a/lib/floating_animation/floating_config.dart +++ b/lib/src/floating_animation/floating_config.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import '../constants.dart'; +import '../utils/constants.dart'; class FloatingConfig { /// Configuration for making the card float as per the movement of device or diff --git a/lib/floating_animation/floating_controller.dart b/lib/src/floating_animation/floating_controller.dart similarity index 97% rename from lib/floating_animation/floating_controller.dart rename to lib/src/floating_animation/floating_controller.dart index ce81a34..ba5f8ce 100644 --- a/lib/floating_animation/floating_controller.dart +++ b/lib/src/floating_animation/floating_controller.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/rendering.dart'; -import 'package:flutter_credit_card/constants.dart'; +import '../utils/constants.dart'; import 'floating_event.dart'; class FloatingController { diff --git a/lib/floating_animation/floating_event.dart b/lib/src/floating_animation/floating_event.dart similarity index 91% rename from lib/floating_animation/floating_event.dart rename to lib/src/floating_animation/floating_event.dart index 607dcbe..0539d46 100644 --- a/lib/floating_animation/floating_event.dart +++ b/lib/src/floating_animation/floating_event.dart @@ -1,5 +1,4 @@ -/// The type of floating event. -enum FloatingType { pointer, gyroscope } +import '../utils/enumerations.dart'; class FloatingEvent { const FloatingEvent({ diff --git a/lib/floating_animation/glare_effect_widget.dart b/lib/src/floating_animation/glare_effect_widget.dart similarity index 97% rename from lib/floating_animation/glare_effect_widget.dart rename to lib/src/floating_animation/glare_effect_widget.dart index cc5b386..c239005 100644 --- a/lib/floating_animation/glare_effect_widget.dart +++ b/lib/src/floating_animation/glare_effect_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../constants.dart'; +import '../utils/constants.dart'; class GlareEffectWidget extends StatelessWidget { const GlareEffectWidget({ diff --git a/lib/src/masked_text_controller.dart b/lib/src/masked_text_controller.dart new file mode 100644 index 0000000..7fe5f54 --- /dev/null +++ b/lib/src/masked_text_controller.dart @@ -0,0 +1,123 @@ +import 'package:flutter/widgets.dart'; + +class MaskedTextController extends TextEditingController { + MaskedTextController({ + required this.mask, + super.text, + Map? translator, + }) { + this.translator = translator ?? MaskedTextController.getDefaultTranslator(); + + addListener(() { + final String previous = _lastUpdatedText; + if (beforeChange(previous, text)) { + updateText(text); + afterChange(previous, text); + } else { + updateText(_lastUpdatedText); + } + }); + + updateText(text); + } + + String mask; + + late Map translator; + + Function afterChange = (String previous, String next) {}; + Function beforeChange = (String previous, String next) { + return true; + }; + + String _lastUpdatedText = ''; + + void updateText(String text) { + if (text.isNotEmpty) { + this.text = _applyMask(mask, text); + } else { + this.text = ''; + } + + _lastUpdatedText = this.text; + } + + void updateMask(String mask, {bool moveCursorToEnd = true}) { + this.mask = mask; + updateText(text); + + if (moveCursorToEnd) { + this.moveCursorToEnd(); + } + } + + void moveCursorToEnd() { + final String text = _lastUpdatedText; + selection = TextSelection.fromPosition(TextPosition(offset: text.length)); + } + + @override + set text(String newText) { + if (super.text != newText) { + super.text = newText; + moveCursorToEnd(); + } + } + + static Map getDefaultTranslator() { + return { + 'A': RegExp(r'[A-Za-z]'), + '0': RegExp(r'[0-9]'), + '@': RegExp(r'[A-Za-z0-9]'), + '*': RegExp(r'.*') + }; + } + + String _applyMask(String? mask, String value) { + String result = ''; + + int maskCharIndex = 0; + int valueCharIndex = 0; + + while (true) { + // if mask is ended, break. + if (maskCharIndex == mask!.length) { + break; + } + + // if value is ended, break. + if (valueCharIndex == value.length) { + break; + } + + final String maskChar = mask[maskCharIndex]; + final String valueChar = value[valueCharIndex]; + + // value equals mask, just set + if (maskChar == valueChar) { + result += maskChar; + valueCharIndex += 1; + maskCharIndex += 1; + continue; + } + + // apply translator if match + if (translator.containsKey(maskChar)) { + if (translator[maskChar]!.hasMatch(valueChar)) { + result += valueChar; + maskCharIndex += 1; + } + + valueCharIndex += 1; + continue; + } + + // not masked value, fixed char on mask + result += maskChar; + maskCharIndex += 1; + continue; + } + + return result; + } +} diff --git a/lib/credit_card_brand.dart b/lib/src/models/credit_card_brand.dart similarity index 57% rename from lib/credit_card_brand.dart rename to lib/src/models/credit_card_brand.dart index 967de3e..899a077 100644 --- a/lib/credit_card_brand.dart +++ b/lib/src/models/credit_card_brand.dart @@ -1,4 +1,4 @@ -import 'package:flutter_credit_card/flutter_credit_card.dart'; +import '../utils/enumerations.dart'; class CreditCardBrand { CreditCardBrand(this.brandName); diff --git a/lib/credit_card_model.dart b/lib/src/models/credit_card_model.dart similarity index 100% rename from lib/credit_card_model.dart rename to lib/src/models/credit_card_model.dart diff --git a/lib/custom_card_type_icon.dart b/lib/src/models/custom_card_type_icon.dart similarity index 78% rename from lib/custom_card_type_icon.dart rename to lib/src/models/custom_card_type_icon.dart index bc99467..b73d568 100644 --- a/lib/custom_card_type_icon.dart +++ b/lib/src/models/custom_card_type_icon.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_credit_card/credit_card_widget.dart'; +import 'package:flutter/widgets.dart'; + +import '../utils/enumerations.dart'; class CustomCardTypeIcon { /// A model class to update card image with user defined widget for the diff --git a/lib/glassmorphism_config.dart b/lib/src/models/glassmorphism_config.dart similarity index 100% rename from lib/glassmorphism_config.dart rename to lib/src/models/glassmorphism_config.dart diff --git a/lib/flutter_credit_card_method_channel.dart b/lib/src/plugin/flutter_credit_card_method_channel.dart similarity index 94% rename from lib/flutter_credit_card_method_channel.dart rename to lib/src/plugin/flutter_credit_card_method_channel.dart index 6d47f7f..6679764 100644 --- a/lib/flutter_credit_card_method_channel.dart +++ b/lib/src/plugin/flutter_credit_card_method_channel.dart @@ -2,8 +2,9 @@ import 'dart:io'; import 'package:flutter/services.dart'; -import 'constants.dart'; -import 'floating_animation/floating_event.dart'; +import '../floating_animation/floating_event.dart'; +import '../utils/constants.dart'; +import '../utils/enumerations.dart'; import 'flutter_credit_card_platform_interface.dart'; /// An implementation of [FlutterCreditCardPlatform] that uses method channels. diff --git a/lib/flutter_credit_card_platform_interface.dart b/lib/src/plugin/flutter_credit_card_platform_interface.dart similarity index 96% rename from lib/flutter_credit_card_platform_interface.dart rename to lib/src/plugin/flutter_credit_card_platform_interface.dart index 1920239..94ea516 100644 --- a/lib/flutter_credit_card_platform_interface.dart +++ b/lib/src/plugin/flutter_credit_card_platform_interface.dart @@ -1,6 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'floating_animation/floating_event.dart'; +import '../floating_animation/floating_event.dart'; import 'flutter_credit_card_method_channel.dart'; abstract class FlutterCreditCardPlatform extends PlatformInterface { diff --git a/lib/flutter_credit_card_web.dart b/lib/src/plugin/flutter_credit_card_web.dart similarity index 93% rename from lib/flutter_credit_card_web.dart rename to lib/src/plugin/flutter_credit_card_web.dart index d25cc6d..4a44cd8 100644 --- a/lib/flutter_credit_card_web.dart +++ b/lib/src/plugin/flutter_credit_card_web.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'floating_animation/floating_event.dart'; +import '../floating_animation/floating_event.dart'; import 'flutter_credit_card_platform_interface.dart'; /// A web implementation of the FlutterCreditCardPlatform of the diff --git a/lib/src/utils/asset_paths.dart b/lib/src/utils/asset_paths.dart new file mode 100644 index 0000000..6132a7f --- /dev/null +++ b/lib/src/utils/asset_paths.dart @@ -0,0 +1,13 @@ +class AssetPaths { + const AssetPaths._(); + + static const String visa = 'icons/visa.png'; + static const String rupay = 'icons/rupay.png'; + static const String mastercard = 'icons/mastercard.png'; + static const String americanExpress = 'icons/amex.png'; + static const String unionpay = 'icons/unionpay.png'; + static const String discover = 'icons/discover.png'; + static const String elo = 'icons/elo.png'; + static const String hipercard = 'icons/hipercard.png'; + static const String chip = 'icons/chip.png'; +} diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart new file mode 100644 index 0000000..8b1a848 --- /dev/null +++ b/lib/src/utils/constants.dart @@ -0,0 +1,139 @@ +import 'dart:math'; + +import 'package:flutter/rendering.dart'; + +import 'asset_paths.dart'; +import 'enumerations.dart'; + +class AppConstants { + const AppConstants._(); + + static const String packageName = 'flutter_credit_card'; + static const String fontFamily = 'halter'; + + static const String threeX = 'XXX'; + static const String fourX = 'XXXX'; + static const String sixteenX = 'XXXX XXXX XXXX XXXX'; + static const String cardNumberMask = '0000 0000 0000 0000'; + static const String expiryDateMask = '00/00'; + static const String cvvMask = '0000'; + static const String cvv = 'CVV'; + static const String cardHolderCaps = 'CARD HOLDER'; + static const String cardHolder = 'Card Holder'; + static const String cardNumber = 'Card Number'; + static const String expiryDate = 'Expiry Date'; + static const String expiryDateShort = 'MM/YY'; + static const String validThru = 'VALID\nTHRU'; + static const String cvvValidationMessage = 'Please input a valid CVV'; + static const String dateValidationMessage = 'Please input a valid date'; + static const String numberValidationMessage = 'Please input a valid number'; + + static const double floatWebBreakPoint = 650; + static const double creditCardAspectRatio = 0.5714; + static const double creditCardPadding = 16; + static const double creditCardIconSize = 48; + static const BorderRadius creditCardBorderRadius = BorderRadius.all( + Radius.circular(10), + ); + + static const double minRestBackVel = 0.01; + static const double maxRestBackVel = 0.05; + static const double defaultRestBackVel = 0.8; + + static const Duration fps60 = Duration(microseconds: 16666); + static const Duration fps60Offset = Duration(microseconds: 16667); + static const Duration defaultAnimDuration = Duration(milliseconds: 500); + + /// Color constants + static const Color defaultGlareColor = Color(0xffFFFFFF); + static const Color floatingShadowColor = Color(0x4D000000); + static const Color defaultCardBgColor = Color(0xff1b447b); + + static const double defaultMaximumAngle = pi / 10; + static const double minBlurRadius = 10; + + /// Gyroscope channel constants + static const String gyroMethodChannelName = 'com.simform.flutter_credit_card'; + static const String gyroEventChannelName = + 'com.simform.flutter_credit_card/gyroscope'; + static const String isGyroAvailableMethod = 'isGyroscopeAvailable'; + static const String initiateMethod = 'initiateEvents'; + static const String cancelMethod = 'cancelEvents'; + + /// Credit Card prefix patterns as of March 2019 + /// A [List] represents a range. + /// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55' + static const Map>> cardNumPatterns = + >>{ + CardType.visa: >{ + ['4'], + }, + CardType.rupay: >{ + ['60'], + ['6521'], + ['6522'], + }, + CardType.americanExpress: >{ + ['34'], + ['37'], + }, + CardType.unionpay: >{ + ['62'], + }, + CardType.discover: >{ + ['6011'], + ['622126', '622925'], // China UnionPay co-branded + ['644', '649'], + ['65'] + }, + CardType.mastercard: >{ + ['51', '55'], + ['2221', '2229'], + ['223', '229'], + ['23', '26'], + ['270', '271'], + ['2720'], + }, + CardType.elo: >{ + ['401178'], + ['401179'], + ['438935'], + ['457631'], + ['457632'], + ['431274'], + ['451416'], + ['457393'], + ['504175'], + ['506699', '506778'], + ['509000', '509999'], + ['627780'], + ['636297'], + ['636368'], + ['650031', '650033'], + ['650035', '650051'], + ['650405', '650439'], + ['650485', '650538'], + ['650541', '650598'], + ['650700', '650718'], + ['650720', '650727'], + ['650901', '650978'], + ['651652', '651679'], + ['655000', '655019'], + ['655021', '655058'] + }, + CardType.hipercard: >{ + ['606282'], + }, + }; + + static const Map cardTypeIconAsset = { + CardType.visa: AssetPaths.visa, + CardType.rupay: AssetPaths.rupay, + CardType.americanExpress: AssetPaths.americanExpress, + CardType.mastercard: AssetPaths.mastercard, + CardType.unionpay: AssetPaths.unionpay, + CardType.discover: AssetPaths.discover, + CardType.elo: AssetPaths.elo, + CardType.hipercard: AssetPaths.hipercard, + }; +} diff --git a/lib/src/utils/enumerations.dart b/lib/src/utils/enumerations.dart new file mode 100644 index 0000000..3904d0c --- /dev/null +++ b/lib/src/utils/enumerations.dart @@ -0,0 +1,21 @@ +enum CardType { + otherBrand, + mastercard, + visa, + rupay, + americanExpress, + unionpay, + discover, + elo, + hipercard, +} + +/// The type of floating event. +enum FloatingType { + pointer, + gyroscope; + + bool get isPointer => this == pointer; + + bool get isGyroscope => this == gyroscope; +} diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart new file mode 100644 index 0000000..20ce05d --- /dev/null +++ b/lib/src/utils/extensions.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +extension NullableStringExtension on String? { + bool get isNullOrEmpty => this?.isEmpty ?? true; + + bool get isNotNullAndNotEmpty => this?.isNotEmpty ?? false; +} + +extension OrientationExtension on Orientation { + bool get isPortrait => this == Orientation.portrait; + + bool get isLandscape => this == Orientation.landscape; +} diff --git a/lib/src/utils/helpers.dart b/lib/src/utils/helpers.dart new file mode 100644 index 0000000..da6ab57 --- /dev/null +++ b/lib/src/utils/helpers.dart @@ -0,0 +1,88 @@ +import 'package:flutter/widgets.dart'; + +import '../models/custom_card_type_icon.dart'; +import 'constants.dart'; +import 'enumerations.dart'; + +/// This function determines the Credit Card type based on the cardPatterns +/// and returns it. +CardType detectCCType(String cardNumber) { + //Default card type is other + CardType cardType = CardType.otherBrand; + + if (cardNumber.isEmpty) { + return cardType; + } + + AppConstants.cardNumPatterns.forEach( + (CardType type, Set> patterns) { + for (List patternRange in patterns) { + // Remove any spaces + String ccPatternStr = cardNumber.replaceAll(RegExp(r'\s+\b|\b\s'), ''); + final int rangeLen = patternRange[0].length; + // Trim the Credit Card number string to match the pattern prefix length + if (rangeLen < cardNumber.length) { + ccPatternStr = ccPatternStr.substring(0, rangeLen); + } + + if (patternRange.length > 1) { + // Convert the prefix range into numbers then make sure the + // Credit Card num is in the pattern range. + // Because Strings don't have '>=' type operators + final int ccPrefixAsInt = int.parse(ccPatternStr); + final int startPatternPrefixAsInt = int.parse(patternRange[0]); + final int endPatternPrefixAsInt = int.parse(patternRange[1]); + if (ccPrefixAsInt >= startPatternPrefixAsInt && + ccPrefixAsInt <= endPatternPrefixAsInt) { + // Found a match + cardType = type; + break; + } + } else { + // Just compare the single pattern prefix with the Credit Card prefix + if (ccPatternStr == patternRange[0]) { + // Found a match + cardType = type; + break; + } + } + } + }, + ); + + return cardType; +} + +/// Returns the icon for the card type if detected else will return a +/// [SizedBox]. +Widget getCardTypeImage({ + required List customIcons, + CardType? cardType, +}) { + const Widget blankSpace = + SizedBox.square(dimension: AppConstants.creditCardIconSize); + + if (cardType == null) { + return blankSpace; + } + + return customIcons.firstWhere( + (CustomCardTypeIcon element) => element.cardType == cardType, + orElse: () { + final bool isKnownCardType = + AppConstants.cardTypeIconAsset.containsKey(cardType); + + return CustomCardTypeIcon( + cardType: isKnownCardType ? cardType : CardType.otherBrand, + cardImage: isKnownCardType + ? Image.asset( + AppConstants.cardTypeIconAsset[cardType]!, + height: AppConstants.creditCardIconSize, + width: AppConstants.creditCardIconSize, + package: AppConstants.packageName, + ) + : blankSpace, + ); + }, + ).cardImage; +} diff --git a/lib/src/utils/typedefs.dart b/lib/src/utils/typedefs.dart new file mode 100644 index 0000000..4175242 --- /dev/null +++ b/lib/src/utils/typedefs.dart @@ -0,0 +1,13 @@ +import 'package:flutter/rendering.dart'; + +import '../floating_animation/floating_event.dart'; +import '../models/credit_card_brand.dart'; +import '../models/credit_card_model.dart'; + +typedef CCModelChangeCallback = void Function(CreditCardModel); + +typedef CCBrandChangeCallback = void Function(CreditCardBrand); + +typedef ValidationCallback = String? Function(String?); + +typedef FloatEventCallback = Matrix4 Function(FloatingEvent? event); diff --git a/lib/src/utils/validators.dart b/lib/src/utils/validators.dart new file mode 100644 index 0000000..183f479 --- /dev/null +++ b/lib/src/utils/validators.dart @@ -0,0 +1,41 @@ +class Validators { + const Validators._(); + + static String? cardNumberValidator(String? value, String errorMsg) { + // Validate less that 13 digits +3 white spaces + return (value?.isEmpty ?? true) || (value?.length ?? 16) < 16 + ? errorMsg + : null; + } + + static String? expiryDateValidator(String? value, String errorMsg) { + if (value?.isEmpty ?? true) { + return errorMsg; + } + + final DateTime now = DateTime.now(); + final List date = value!.split(RegExp(r'/')); + + final int month = int.parse(date.first); + final int year = int.parse('20${date.last}'); + + final int lastDayOfMonth = month < 12 + ? DateTime(year, month + 1, 0).day + : DateTime(year + 1, 1, 0).day; + + final DateTime cardDate = + DateTime(year, month, lastDayOfMonth, 23, 59, 59, 999); + + if (cardDate.isBefore(now) || month > 12 || month == 0) { + return errorMsg; + } + + return null; + } + + static String? cvvValidator(String? value, String errorMsg) { + return (value?.isEmpty ?? true) || ((value?.length ?? 3) < 3) + ? errorMsg + : null; + } +}