diff --git a/android/build.gradle b/android/build.gradle index 46409379..6688e1ce 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,8 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdk 31 + namespace 'com.simform.flutter_credit_card' + compileSdk 34 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 03ef62cf..a2f47b60 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index c4488639..399805f8 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -39,7 +39,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.simform.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/lib/main.dart b/example/lib/main.dart index ba6c502d..15fd22c0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,6 +17,7 @@ class MySampleState extends State { String cardNumber = ''; String expiryDate = ''; String cardHolderName = ''; + String bankName = ''; String cvvCode = ''; bool isCvvFocused = false; bool useGlassMorphism = false; @@ -105,13 +106,17 @@ class MySampleState extends State { ), ), CreditCardWidget( + onItemTapped: (TappedItem tapedItem, String content) { + print( + 'tap item==>${tapedItem.toString()},content==>$content'); + }, enableFloatingCard: useFloatingAnimation, glassmorphismConfig: _getGlassmorphismConfig(), cardNumber: cardNumber, expiryDate: expiryDate, cardHolderName: cardHolderName, cvvCode: cvvCode, - bankName: 'Axis Bank', + bankName: bankName, frontCardBorder: useGlassMorphism ? null : Border.all(color: Colors.grey), @@ -151,28 +156,30 @@ class MySampleState extends State { obscureNumber: true, cardNumber: cardNumber, cvvCode: cvvCode, + bankName: bankName, isHolderNameVisible: true, isCardNumberVisible: true, isExpiryDateVisible: true, cardHolderName: cardHolderName, expiryDate: expiryDate, inputConfiguration: const InputConfiguration( - cardNumberDecoration: InputDecoration( - labelText: 'Number', - hintText: 'XXXX XXXX XXXX XXXX', - ), - expiryDateDecoration: InputDecoration( - labelText: 'Expired Date', - hintText: 'XX/XX', - ), - cvvCodeDecoration: InputDecoration( - labelText: 'CVV', - hintText: 'XXX', - ), - cardHolderDecoration: InputDecoration( - labelText: 'Card Holder', - ), - ), + cardNumberDecoration: InputDecoration( + labelText: 'Number', + hintText: 'XXXX XXXX XXXX XXXX', + ), + expiryDateDecoration: InputDecoration( + labelText: 'Expired Date', + hintText: 'XX/XX', + ), + cvvCodeDecoration: InputDecoration( + labelText: 'CVV', + hintText: 'XXX', + ), + cardHolderDecoration: InputDecoration( + labelText: 'Card Holder', + ), + bankNameDecoration: InputDecoration( + labelText: 'Default Bank')), onCreditCardModelChange: onCreditCardModelChange, ), const SizedBox(height: 20), @@ -320,6 +327,7 @@ class MySampleState extends State { cardNumber = creditCardModel.cardNumber; expiryDate = creditCardModel.expiryDate; cardHolderName = creditCardModel.cardHolderName; + bankName = creditCardModel.bankName; cvvCode = creditCardModel.cvvCode; isCvvFocused = creditCardModel.isCvvFocused; }); diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 00000000..c795730d --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 00000000..79862f06 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.12.1 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 7d9b5551..df74506b 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 24A695BD09C452D813B6AB93 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E45C98BDA00C011CD89254F5 /* Pods_RunnerTests.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; @@ -61,6 +62,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0CA6FEAB2B4792E93AFD5430 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1678991DE9E8EAB11CFD892D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -83,6 +86,8 @@ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; A6A0E03E1578A1629B37A597 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; DDC969ADD6BD8A10FD973D26 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E45C98BDA00C011CD89254F5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F51F0E7189BC39C5D2ED0EE0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 24A695BD09C452D813B6AB93 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -183,6 +189,7 @@ isa = PBXGroup; children = ( 6D566C8D96BAB46777E3D3CF /* Pods_Runner.framework */, + E45C98BDA00C011CD89254F5 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -193,6 +200,9 @@ A6A0E03E1578A1629B37A597 /* Pods-Runner.debug.xcconfig */, 8E4A00D932DD3F713F3FD242 /* Pods-Runner.release.xcconfig */, DDC969ADD6BD8A10FD973D26 /* Pods-Runner.profile.xcconfig */, + 0CA6FEAB2B4792E93AFD5430 /* Pods-RunnerTests.debug.xcconfig */, + F51F0E7189BC39C5D2ED0EE0 /* Pods-RunnerTests.release.xcconfig */, + 1678991DE9E8EAB11CFD892D /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; @@ -205,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 6C0A9FB2D7FCE4EEE2179D71 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -247,7 +258,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -370,6 +381,28 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 6C0A9FB2D7FCE4EEE2179D71 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -421,6 +454,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0CA6FEAB2B4792E93AFD5430 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -435,6 +469,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F51F0E7189BC39C5D2ED0EE0 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -449,6 +484,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1678991DE9E8EAB11CFD892D /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8fedab68..397f3d33 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ [ - Container( - margin: EdgeInsets.all(padding), - width: width ?? screenWidth, - height: height ?? implicitHeight, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: AppConstants.creditCardBorderRadius, - boxShadow: shadowConfig != null && floatingController != null - ? [ - BoxShadow( - blurRadius: shadowConfig!.blurRadius, - color: shadowConfig!.color, - offset: shadowConfig!.offset + - Offset( - floatingController!.y * 100, - -floatingController!.x * 100, - ), - ), - ] - : null, - border: border, - gradient: - glassmorphismConfig?.gradient ?? backgroundGradientColor, - image: backgroundImage?.isNotEmpty ?? false - ? DecorationImage( - image: ExactAssetImage(backgroundImage!), - fit: BoxFit.fill, - ) - : backgroundNetworkImage?.isNotEmpty ?? false - ? DecorationImage( - image: NetworkImage(backgroundNetworkImage!), - fit: BoxFit.fill, - ) - : null, - ), - child: GlareEffectWidget( - border: border, - glarePosition: glarePosition, - child: glassmorphismConfig == null - ? child - : BackdropFilter( - filter: ImageFilter.blur( - sigmaX: glassmorphismConfig!.blurX, - sigmaY: glassmorphismConfig!.blurY, + ClipRRect( + child: Container( + margin: EdgeInsets.all(padding), + width: width ?? screenWidth, + height: height ?? implicitHeight, + clipBehavior: Clip.antiAliasWithSaveLayer, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: AppConstants.creditCardBorderRadius, + boxShadow: shadowConfig != null && floatingController != null + ? [ + BoxShadow( + blurRadius: shadowConfig!.blurRadius, + color: shadowConfig!.color, + offset: shadowConfig!.offset + + Offset( + floatingController!.y * 100, + -floatingController!.x * 100, + ), + ), + ] + : null, + border: border, + gradient: + glassmorphismConfig?.gradient ?? backgroundGradientColor, + image: backgroundImage?.isNotEmpty ?? false + ? DecorationImage( + image: ExactAssetImage(backgroundImage!), + fit: BoxFit.fill, + ) + : backgroundNetworkImage?.isNotEmpty ?? false + ? DecorationImage( + image: NetworkImage(backgroundNetworkImage!), + fit: BoxFit.fill, + ) + : null, + ), + child: GlareEffectWidget( + border: border, + glarePosition: glarePosition, + child: glassmorphismConfig == null + ? child + : BackdropFilter( + filter: ImageFilter.blur( + sigmaX: glassmorphismConfig!.blurX, + sigmaY: glassmorphismConfig!.blurY, + ), + child: child, ), - child: child, - ), + ), ), ), if (glassmorphismConfig != null) diff --git a/lib/src/credit_card_form.dart b/lib/src/credit_card_form.dart index e8b0a36c..0d85350e 100644 --- a/lib/src/credit_card_form.dart +++ b/lib/src/credit_card_form.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_credit_card/src/utils/helpers.dart'; import '../flutter_credit_card.dart'; import 'masked_text_controller.dart'; @@ -12,18 +13,21 @@ class CreditCardForm extends StatefulWidget { required this.expiryDate, required this.cardHolderName, required this.cvvCode, + required this.bankName, required this.onCreditCardModelChange, - required this.formKey, + this.formKey, this.obscureCvv = false, this.obscureNumber = false, this.inputConfiguration = const InputConfiguration(), this.cardNumberKey, this.cardHolderKey, + this.bankNameKey, this.expiryDateKey, this.cvvCodeKey, this.cvvValidationMessage = AppConstants.cvvValidationMessage, this.dateValidationMessage = AppConstants.dateValidationMessage, this.numberValidationMessage = AppConstants.numberValidationMessage, + this.isBankNameVisible = true, this.isHolderNameVisible = true, this.isCardNumberVisible = true, this.isExpiryDateVisible = true, @@ -33,6 +37,7 @@ class CreditCardForm extends StatefulWidget { this.expiryDateValidator, this.cvvValidator, this.cardHolderValidator, + this.bankNameValidator, this.onFormComplete, this.disableCardNumberAutoFillHints = false, super.key, @@ -50,6 +55,9 @@ class CreditCardForm extends StatefulWidget { /// A string indicating cvv code in the text field. final String cvvCode; + /// A string indicating bank name code in the text field. + final String bankName; + /// Error message string when invalid cvv is entered. final String cvvValidationMessage; @@ -74,6 +82,9 @@ class CreditCardForm extends StatefulWidget { /// Defaults to true. final bool isHolderNameVisible; + /// is bank name visible + final bool isBankNameVisible; + /// Allow editing the credit card number by enabling this in the credit /// card form. Defaults to true. final bool isCardNumberVisible; @@ -87,7 +98,7 @@ class CreditCardForm extends StatefulWidget { final bool isExpiryDateVisible; /// A form state key for this credit card form. - final GlobalKey formKey; + final GlobalKey? formKey; /// Provides a callback when text field provides callback in /// [onEditingComplete]. @@ -99,6 +110,9 @@ class CreditCardForm extends StatefulWidget { /// A FormFieldState key for card holder text field. final GlobalKey>? cardHolderKey; + /// A FormFieldState key for bank name text field. + final GlobalKey>? bankNameKey; + /// A FormFieldState key for expiry date text field. final GlobalKey>? expiryDateKey; @@ -123,6 +137,8 @@ class CreditCardForm extends StatefulWidget { /// A validator for card holder text field. final ValidationCallback? cardHolderValidator; + final ValidationCallback? bankNameValidator; + /// 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 /// credit card numbers it shows keyboard with characters. But, disabling @@ -143,45 +159,69 @@ class _CreditCardFormState extends State { late String expiryDate; late String cardHolderName; late String cvvCode; + late String bankName; bool isCvvFocused = false; late final CreditCardModel creditCardModel; late final CCModelChangeCallback onCreditCardModelChange = widget.onCreditCardModelChange; - late final MaskedTextController _cardNumberController = MaskedTextController( - mask: AppConstants.cardNumberMask, - text: widget.cardNumber, - ); + late MaskedTextController _cardNumberController; + + late TextEditingController _expiryDateController; - late final TextEditingController _expiryDateController = MaskedTextController( - mask: AppConstants.expiryDateMask, - text: widget.expiryDate, - ); + late TextEditingController _cardHolderNameController; - late final TextEditingController _cardHolderNameController = - TextEditingController( - text: widget.cardHolderName, - ); + late TextEditingController _bankNameController; - late final TextEditingController _cvvCodeController = MaskedTextController( - mask: AppConstants.cvvMask, - text: widget.cvvCode, - ); + late TextEditingController _cvvCodeController; final FocusNode cvvFocusNode = FocusNode(); final FocusNode expiryDateNode = FocusNode(); final FocusNode cardHolderNode = FocusNode(); + final FocusNode bankNameNode = FocusNode(); + + void initTextControllers() { + _cardNumberController = MaskedTextController( + mask: AppConstants.cardNumberMask, + text: widget.cardNumber, + ); + _expiryDateController = MaskedTextController( + mask: AppConstants.expiryDateMask, + text: widget.expiryDate, + ); + _cardHolderNameController = TextEditingController( + text: widget.cardHolderName, + ); + _bankNameController = TextEditingController( + text: widget.bankName, + ); + _cvvCodeController = MaskedTextController( + mask: AppConstants.cvvMask, + text: widget.cvvCode, + ); + } @override void initState() { super.initState(); + initTextControllers(); createCreditCardModel(); cvvFocusNode.addListener(textFieldFocusDidChange); } + void resetControllersValue() { + _cardNumberController.text = widget.cardNumber; + _expiryDateController.text = widget.expiryDate; + _cardHolderNameController.text = widget.cardHolderName; + _bankNameController.text = widget.bankName; + _cvvCodeController.text = widget.cvvCode; + } + + @override Widget build(BuildContext context) { + resetControllersValue(); return Form( key: widget.formKey, child: Column( @@ -189,8 +229,8 @@ class _CreditCardFormState extends State { Visibility( visible: widget.isCardNumberVisible, child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - margin: const EdgeInsets.only(left: 16, top: 16, right: 16), + padding: const EdgeInsets.symmetric(vertical: 0.0), + margin: const EdgeInsets.only(left: 8, top: 0, right: 8), child: TextFormField( key: widget.cardNumberKey, obscureText: widget.obscureNumber, @@ -220,8 +260,8 @@ class _CreditCardFormState extends State { visible: widget.isExpiryDateVisible, child: Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - margin: const EdgeInsets.only(left: 16, top: 8, right: 16), + padding: const EdgeInsets.symmetric(vertical: 0.0), + margin: const EdgeInsets.only(left: 8, top: 0, right: 8), child: TextFormField( key: widget.expiryDateKey, controller: _expiryDateController, @@ -251,8 +291,8 @@ class _CreditCardFormState extends State { child: Visibility( visible: widget.enableCvv, child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - margin: const EdgeInsets.only(left: 16, top: 8, right: 16), + padding: const EdgeInsets.symmetric(vertical: 0.0), + margin: const EdgeInsets.only(left: 8, top: 0, right: 8), child: TextFormField( key: widget.cvvCodeKey, obscureText: widget.obscureCvv, @@ -284,8 +324,8 @@ class _CreditCardFormState extends State { Visibility( visible: widget.isHolderNameVisible, child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - margin: const EdgeInsets.only(left: 16, top: 8, right: 16), + padding: const EdgeInsets.symmetric(vertical: 0.0), + margin: const EdgeInsets.only(left: 8, top: 0, right: 8), child: TextFormField( key: widget.cardHolderKey, controller: _cardHolderNameController, @@ -295,13 +335,36 @@ class _CreditCardFormState extends State { style: widget.inputConfiguration.cardHolderTextStyle, keyboardType: TextInputType.text, autovalidateMode: widget.autovalidateMode, - textInputAction: TextInputAction.done, + textInputAction: widget.isBankNameVisible + ? TextInputAction.next + : TextInputAction.done, autofillHints: const [AutofillHints.creditCardName], onEditingComplete: _onHolderNameEditComplete, validator: widget.cardHolderValidator, ), ), ), + Visibility( + visible: widget.isBankNameVisible, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 0.0), + margin: const EdgeInsets.only(left: 8, top: 0, right: 8), + child: TextFormField( + key: widget.bankNameKey, + controller: _bankNameController, + onChanged: _onBankNameChange, + focusNode: bankNameNode, + decoration: widget.inputConfiguration.bankNameDecoration, + style: widget.inputConfiguration.bankNameTextStyle, + keyboardType: TextInputType.text, + autovalidateMode: widget.autovalidateMode, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.creditCardName], + onEditingComplete: _onBankNameEditComplete, + validator: widget.bankNameValidator, + ), + ), + ), ], ), ); @@ -317,6 +380,7 @@ class _CreditCardFormState extends State { void textFieldFocusDidChange() { isCvvFocused = creditCardModel.isCvvFocused = cvvFocusNode.hasFocus; + onCreditCardModelChange(creditCardModel); } @@ -325,6 +389,7 @@ class _CreditCardFormState extends State { expiryDate = widget.expiryDate; cardHolderName = widget.cardHolderName; cvvCode = widget.cvvCode; + bankName = widget.bankName; creditCardModel = CreditCardModel( cardNumber, @@ -332,12 +397,15 @@ class _CreditCardFormState extends State { cardHolderName, cvvCode, isCvvFocused, + bankName, + detectCCType(cardNumber).name, ); } void _onCardNumberChange(String value) { setState(() { creditCardModel.cardNumber = cardNumber = _cardNumberController.text; + creditCardModel.type = detectCCType(cardNumber).name; onCreditCardModelChange(creditCardModel); }); } @@ -359,6 +427,13 @@ class _CreditCardFormState extends State { }); } + void _onBankNameChange(String text) { + setState(() { + creditCardModel.bankName = bankName = text; + onCreditCardModelChange(creditCardModel); + }); + } + void _onCardHolderNameChange(String value) { setState(() { creditCardModel.cardHolderName = @@ -378,6 +453,16 @@ class _CreditCardFormState extends State { } void _onHolderNameEditComplete() { + if (widget.isBankNameVisible) { + FocusScope.of(context).requestFocus(bankNameNode); + } else { + FocusScope.of(context).unfocus(); + onCreditCardModelChange(creditCardModel); + widget.onFormComplete?.call(); + } + } + + void _onBankNameEditComplete() { FocusScope.of(context).unfocus(); onCreditCardModelChange(creditCardModel); widget.onFormComplete?.call(); diff --git a/lib/src/credit_card_widget.dart b/lib/src/credit_card_widget.dart index 8b55432a..93e5abdf 100644 --- a/lib/src/credit_card_widget.dart +++ b/lib/src/credit_card_widget.dart @@ -21,9 +21,12 @@ import 'utils/extensions.dart'; import 'utils/helpers.dart'; import 'utils/typedefs.dart'; +enum TappedItem { cardNumber, expiryDate, cardHolderName, cvvCode, bankName } + class CreditCardWidget extends StatefulWidget { /// A widget showcasing credit card UI. const CreditCardWidget({ + this.onItemTapped, required this.cardNumber, required this.expiryDate, required this.cardHolderName, @@ -59,6 +62,8 @@ class CreditCardWidget extends StatefulWidget { super.key, }); + final void Function(TappedItem tapedItem, String content)? onItemTapped; + /// A string indicating number on the card. final String cardNumber; @@ -469,6 +474,8 @@ class _CreditCardWidgetState extends State required String number, required TextStyle defaultTextStyle, }) { + final Null Function(DragEndDetails details)? emptyPanEnd = widget.onItemTapped == null + ? null : (DragEndDetails details) {};//Prevent accidental touches return CardBackground( glarePosition: glarePosition, floatingController: widget.enableFloatingCard ? floatController : null, @@ -487,13 +494,22 @@ class _CreditCardWidgetState extends State if (widget.bankName.isNotNullAndNotEmpty) Align( alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 16, top: 16), - child: Text( - widget.bankName!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: defaultTextStyle, + child: GestureDetector( + onPanEnd: emptyPanEnd, + onTap: widget.onItemTapped == null + ? null + : () { + widget.onItemTapped + ?.call(TappedItem.bankName, widget.bankName!); + }, + child: Padding( + padding: const EdgeInsets.only(right: 16, top: 16), + child: Text( + widget.bankName!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: defaultTextStyle, + ), ), ), ), @@ -517,62 +533,98 @@ class _CreditCardWidgetState extends State ), const SizedBox(height: 10), Expanded( - child: Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: Text( - widget.cardNumber.isEmpty ? AppConstants.sixteenX : number, - style: widget.textStyle ?? defaultTextStyle, + child: GestureDetector( + onPanEnd: emptyPanEnd,//Prevent accidental touches + onTap: widget.onItemTapped == null + ? null + : () { + widget.onItemTapped?.call( + TappedItem.cardNumber, + widget.cardNumber.isEmpty + ? AppConstants.sixteenX + : widget.cardNumber); + }, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Text( + widget.cardNumber.isEmpty ? AppConstants.sixteenX : number, + style: widget.textStyle ?? defaultTextStyle, + ), ), ), ), Expanded( flex: 1, + child: GestureDetector( + onPanEnd: emptyPanEnd,//Prevent accidental touches + onTap: widget.onItemTapped == null + ? null + : () { + widget.onItemTapped?.call( + TappedItem.expiryDate, + widget.expiryDate.isEmpty + ? widget.labelExpiredDate + : widget.expiryDate); + }, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.labelValidThru, + style: widget.textStyle ?? + defaultTextStyle.copyWith(fontSize: 7), + textAlign: TextAlign.center, + ), + const SizedBox(width: 5), + Text( + widget.expiryDate.isEmpty + ? widget.labelExpiredDate + : widget.expiryDate, + style: widget.textStyle ?? defaultTextStyle, + ), + ], + ), + ), + ), + ), + GestureDetector( + onPanEnd: emptyPanEnd,//Prevent accidental touches + onTap: widget.onItemTapped == null + ? null + : () { + widget.onItemTapped?.call( + TappedItem.cardHolderName, + widget.cardHolderName.isEmpty + ? widget.labelCardHolder + : widget.cardHolderName); + }, child: Padding( - padding: const EdgeInsetsDirectional.only(start: 16), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), child: Row( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - widget.labelValidThru, - style: widget.textStyle ?? - defaultTextStyle.copyWith(fontSize: 7), - textAlign: TextAlign.center, - ), - const SizedBox(width: 5), - Text( - widget.expiryDate.isEmpty - ? widget.labelExpiredDate - : widget.expiryDate, - style: widget.textStyle ?? defaultTextStyle, + Visibility( + visible: widget.isHolderNameVisible, + child: Expanded( + child: Text( + widget.cardHolderName.isEmpty + ? widget.labelCardHolder + : widget.cardHolderName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: widget.textStyle ?? defaultTextStyle, + ), + ), ), + _getCardTypeIcon(), ], ), ), ), - Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Visibility( - visible: widget.isHolderNameVisible, - child: Expanded( - child: Text( - widget.cardHolderName.isEmpty - ? widget.labelCardHolder - : widget.cardHolderName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: widget.textStyle ?? defaultTextStyle, - ), - ), - ), - _getCardTypeIcon(), - ], - ), - ), ], ), ); @@ -653,16 +705,30 @@ class _CreditCardWidgetState extends State flex: 3, child: Container( color: Colors.white, - child: Padding( - padding: const EdgeInsets.all(5), - child: Text( - widget.cvvCode.isEmpty - ? isAmex - ? AppConstants.fourX - : AppConstants.threeX - : cvv, - maxLines: 1, - style: widget.textStyle ?? defaultTextStyle, + child: GestureDetector( + onPanEnd: (DragEndDetails details) {},//Prevent accidental touches + onTap: widget.onItemTapped == null + ? null + : () { + widget.onItemTapped?.call( + TappedItem.cvvCode, + widget.cvvCode.isEmpty + ? isAmex + ? AppConstants.fourX + : AppConstants.threeX + : cvv); + }, + child: Padding( + padding: const EdgeInsets.all(5), + child: Text( + widget.cvvCode.isEmpty + ? isAmex + ? AppConstants.fourX + : AppConstants.threeX + : cvv, + maxLines: 1, + style: widget.textStyle ?? defaultTextStyle, + ), ), ), ), diff --git a/lib/src/floating_animation/glare_effect_widget.dart b/lib/src/floating_animation/glare_effect_widget.dart index c2390052..3e6295f4 100644 --- a/lib/src/floating_animation/glare_effect_widget.dart +++ b/lib/src/floating_animation/glare_effect_widget.dart @@ -30,15 +30,17 @@ class GlareEffectWidget extends StatelessWidget { child, if (glarePosition != null) Positioned.fill( - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: border, - gradient: LinearGradient( - tileMode: TileMode.clamp, - colors: _glareGradientColors, - stops: _gradientStop, - transform: GradientRotation(glarePosition!), + child: IgnorePointer( + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: border, + gradient: LinearGradient( + tileMode: TileMode.clamp, + colors: _glareGradientColors, + stops: _gradientStop, + transform: GradientRotation(glarePosition!), + ), ), ), ), diff --git a/lib/src/models/credit_card_model.dart b/lib/src/models/credit_card_model.dart index d7d9c3f9..f3763437 100644 --- a/lib/src/models/credit_card_model.dart +++ b/lib/src/models/credit_card_model.dart @@ -1,6 +1,6 @@ class CreditCardModel { CreditCardModel(this.cardNumber, this.expiryDate, this.cardHolderName, - this.cvvCode, this.isCvvFocused); + this.cvvCode, this.isCvvFocused,this.bankName,this.type); /// Number of the credit/debit card. String cardNumber = ''; @@ -14,6 +14,11 @@ class CreditCardModel { /// Cvv code on card. String cvvCode = ''; + /// Bank name on card. + String bankName = ''; + /// A boolean for indicating if cvv is focused or not. bool isCvvFocused = false; + + String type = ''; } diff --git a/lib/src/models/input_configuration.dart b/lib/src/models/input_configuration.dart index b4f77901..83e60b2c 100644 --- a/lib/src/models/input_configuration.dart +++ b/lib/src/models/input_configuration.dart @@ -19,10 +19,14 @@ class InputConfiguration { labelText: AppConstants.cvv, hintText: AppConstants.threeX, ), + this.bankNameDecoration = const InputDecoration( + labelText: AppConstants.bankName, + ), this.cardNumberTextStyle, this.cardHolderTextStyle, this.expiryDateTextStyle, this.cvvCodeTextStyle, + this.bankNameTextStyle, }); /// Provides decoration to card number text field. @@ -37,6 +41,9 @@ class InputConfiguration { /// Provides decoration to cvv code text field. final InputDecoration cvvCodeDecoration; + /// Provides decoration to bank name text field. + final InputDecoration bankNameDecoration; + /// Provides textStyle to card number text field. final TextStyle? cardNumberTextStyle; @@ -48,4 +55,7 @@ class InputConfiguration { /// Provides textStyle to cvv code text field. final TextStyle? cvvCodeTextStyle; + + /// Provides textStyle to bank name text field. + final TextStyle? bankNameTextStyle; } diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 350214cf..60f5415d 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -11,6 +11,7 @@ class AppConstants { static const String packageName = 'flutter_credit_card'; static const String fontFamily = 'halter'; + static const String bankName = 'Bank Name'; static const String threeX = 'XXX'; static const String fourX = 'XXXX'; static const String sixteenX = 'XXXX XXXX XXXX XXXX';