Formulário de entrada de cartão de crédito do Flutter – No Packages
É comum usar pacotes de terceiros ao adicionar um formulário de cartão de crédito ao Flutter. Mostrarei a você uma maneira fácil de adicionar um formulário de cartão de crédito no Flutter sem usar nenhum pacote de terceiros.
Você aprenderá a identificar o tipo de cartão, se é um Visa ou um MasterCard, e como adicionar espaços após os quatro dígitos e adicionar uma barra após o mês. Você também aprenderá a validar o número do cartão.
Vamos começar, a tela é bastante simples, com alguns campos de texto e um botão.
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class AddNewCardScreen extends StatefulWidget { const AddNewCardScreen({Key? key}) : super(key: key); @override State<AddNewCardScreen> createState() => _AddNewCardScreenState(); } class _AddNewCardScreenState extends State<AddNewCardScreen> { TextEditingController cardNumberController = TextEditingController(); CardType cardType = CardType.Invalid; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar(title: const Text("New card")), body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ const Spacer(), Form( child: Column( children: [ TextFormField( controller: cardNumberController, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(19), ], decoration: InputDecoration(hintText: "Card number"), ), Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: TextFormField( decoration: const InputDecoration(hintText: "Full name"), ), ), Row( children: [ Expanded( child: TextFormField( keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, // Limit the input LengthLimitingTextInputFormatter(4), ], decoration: const InputDecoration(hintText: "CVV"), ), ), const SizedBox(width: 16), Expanded( child: TextFormField( keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(5), ], decoration: const InputDecoration(hintText: "MM/YY"), ), ), ], ), ], ), ), const Spacer(flex: 2), Padding( padding: const EdgeInsets.only(top: 16), child: ElevatedButton( child: const Text("Add card"), onPressed: () {}, ), ), const Spacer(), ], ), ), ), ); } }
Como o cartão ainda não foi criado, é exibido um erro no tipo de cartão.
enum CardType { Master, Visa, Verve, Discover, AmericanExpress, DinersClub, Jcb, Others, Invalid }
Ao inserir o número do cartão de crédito, você perceberá que ele adiciona espaços após o quarto dígito. O TextInputFormatter
nos permite fazer essas coisas no Flutter. Agora vamos criar nosso próprio CardNumberInputFormatter
.
class CardNumberInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { var text = newValue.text; if (newValue.selection.baseOffset == 0) { return newValue; } var buffer = StringBuffer(); for (int i = 0; i < text.length; i++) { buffer.write(text[i]); var nonZeroIndex = i + 1; if (nonZeroIndex % 4 == 0 && nonZeroIndex != text.length) { buffer.write(' '); // Add double spaces. } } var string = buffer.toString(); return newValue.copyWith( text: string, selection: TextSelection.collapsed(offset: string.length)); } }
Em seguida, volte ao TextFormField do cartão de crédito e adicione CardNumberInputFormatter
a inputFormatters
.
... TextFormField( controller: creditCardController, keyboardType: TextInputType.number, validator: CardUtils.validateCardNum, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(19), CardNumberInputFormatter(), ], decoration: InputDecoration(hintText: "Card number"), ),
Vamos dar uma olhada na prévia👇
Precisamos fazer algo semelhante para a data de expiração, mas, em vez de espaços, precisamos de uma barra.
class CardMonthInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { var newText = newValue.text; if (newValue.selection.baseOffset == 0) { return newValue; } var buffer = StringBuffer(); for (int i = 0; i < newText.length; i++) { buffer.write(newText[i]); var nonZeroIndex = i + 1; if (nonZeroIndex % 2 == 0 && nonZeroIndex != newText.length) { buffer.write('/'); } } var string = buffer.toString(); return newValue.copyWith( text: string, selection: TextSelection.collapsed(offset: string.length)); } }
Em inputFormatters, defina CardMonthInputFormatter.
TextFormField( keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(4), CardMonthInputFormatter(), ], decoration: const InputDecoration(hintText: "MM/YY"), ),
A próxima etapa é identificar o tipo de cartão, se é Visa, Mastercard ou outro. Gostaria muito de tê-lo no CardUtils
.
class CardUtils { static CardType getCardTypeFrmNumber(String input) { CardType cardType; if (input.startsWith(RegExp( r'((5[1-5])|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720))'))) { cardType = CardType.Master; } else if (input.startsWith(RegExp(r'[4]'))) { cardType = CardType.Visa; } else if (input.startsWith(RegExp(r'((506(0|1))|(507(8|9))|(6500))'))) { cardType = CardType.Verve; } else if (input.startsWith(RegExp(r'((34)|(37))'))) { cardType = CardType.AmericanExpress; } else if (input.startsWith(RegExp(r'((6[45])|(6011))'))) { cardType = CardType.Discover; } else if (input.startsWith(RegExp(r'((30[0-5])|(3[89])|(36)|(3095))'))) { cardType = CardType.DinersClub; } else if (input.startsWith(RegExp(r'(352[89]|35[3-8][0-9])'))) { cardType = CardType.Jcb; } else if (input.length <= 8) { cardType = CardType.Others; } else { cardType = CardType.Invalid; } return cardType; } }
Sabendo o tipo de cartão de crédito, é muito fácil mostrar seu logotipo.
class CardUtils { ... static Widget? getCardIcon(CardType? cardType) { String img = ""; Icon? icon; switch (cardType) { case CardType.Master: img = 'mastercard.png'; break; case CardType.Visa: img = 'visa.png'; break; case CardType.Verve: img = 'verve.png'; break; case CardType.AmericanExpress: img = 'american_express.png'; break; case CardType.Discover: img = 'discover.png'; break; case CardType.DinersClub: img = 'dinners_club.png'; break; case CardType.Jcb: img = 'jcb.png'; break; case CardType.Others: icon = const Icon( Icons.credit_card, size: 24.0, color: Color(0xFFB8B5C3), ); break; default: icon = const Icon( Icons.warning, size: 24.0, color: Color(0xFFB8B5C3), ); break; } Widget? widget; if (img.isNotEmpty) { widget = Image.asset( 'assets/images/$img', width: 40.0, ); } else { widget = icon; } return widget; } }
Para melhorar a experiência do usuário, usamos espaços duplos após 4 dígitos no número do cartão. O número do cartão precisa estar sem esses espaços duplos, portanto, definiremos um método chamado getCleanedNumber.
class CardUtils { ... static String getCleanedNumber(String text) { RegExp regExp = RegExp(r"[^0-9]"); return text.replaceAll(regExp, ''); } }
Em AddNewCardScreen
, defina um método que nos permita saber o tipo de cartão e atualizá-lo adequadamente.
void getCardTypeFrmNumber() { if (cardNumberController.text.length <= 6) { String input = CardUtils.getCleanedNumber(cardNumberController.text); CardType type = CardUtils.getCardTypeFrmNumber(input); if (type != cardType) { setState(() { cardType = type; }); } } }
Sempre que o campo de texto do cartão de crédito for alterado, devemos executar o método getCardTypeFrmNumber
. Você pode fazer isso adicionando um listener ao cardNumberController
em initState
.
@override void initState() { cardNumberController.addListener( () { getCardTypeFrmNumber(); }, ); super.initState(); }
Não se esqueça de descartá-lo.
@override void dispose() { cardNumberController.dispose(); super.dispose(); }
De volta ao campo de texto do cartão de crédito, remova o const
de InputDecoration
e adicione o suffix
que representa o logotipo do tipo de cartão.
TextFormField( ... decoration: InputDecoration( hintText: "Card number", suffix: CardUtils.getCardIcon(cardType), ), ),
Ainda não validamos o cartão de crédito, que é a etapa mais importante.
class CardUtils { ... /// With the card number with Luhn Algorithm /// https://en.wikipedia.org/wiki/Luhn_algorithm static String? validateCardNum(String? input) { if (input == null || input.isEmpty) { return "This field is required"; } input = getCleanedNumber(input); if (input.length < 8) { return "Card is invalid"; } int sum = 0; int length = input.length; for (var i = 0; i < length; i++) { // get digits in reverse order int digit = int.parse(input[length - i - 1]); // every 2nd number multiply with 2 if (i % 2 == 1) { digit *= 2; } sum += digit > 9 ? (digit - 9) : digit; } if (sum % 10 == 0) { return null; } return "Card is invalid"; } }
Não é apenas o número do cartão que precisa ser validado, mas também o CVV e a data de validade.
class CardUtils { ... static String? validateCVV(String? value) { if (value == null || value.isEmpty) { return "This field is required"; } if (value.length < 3 || value.length > 4) { return "CVV is invalid"; } return null; } static String? validateDate(String? value) { if (value == null || value.isEmpty) { return "This field is required"; } int year; int month; if (value.contains(RegExp(r'(/)'))) { var split = value.split(RegExp(r'(/)')); month = int.parse(split[0]); year = int.parse(split[1]); } else { month = int.parse(value.substring(0, (value.length))); year = -1; // Lets use an invalid year intentionally } if ((month < 1) || (month > 12)) { // A valid month is between 1 (January) and 12 (December) return 'Expiry month is invalid'; } var fourDigitsYear = convertYearTo4Digits(year); if ((fourDigitsYear < 1) || (fourDigitsYear > 2099)) { // We are assuming a valid should be between 1 and 2099. // Note that, it's valid doesn't mean that it has not expired. return 'Expiry year is invalid'; } if (!hasDateExpired(month, year)) { return "Card has expired"; } return null; } }
Basta adicionar esses validadores aos campos de texto e pronto. Aqui estão alguns métodos de bônus 🥳 que podem ser úteis para você.
/// Convert the two-digit year to four-digit year if necessary static int convertYearTo4Digits(int year) { if (year < 100 && year >= 0) { var now = DateTime.now(); String currentYear = now.year.toString(); String prefix = currentYear.substring(0, currentYear.length - 2); year = int.parse('$prefix${year.toString().padLeft(2, '0')}'); } return year; } static bool hasDateExpired(int month, int year) { return isNotExpired(year, month); } static bool isNotExpired(int year, int month) { // It has not expired if both the year and date has not passed return !hasYearPassed(year) && !hasMonthPassed(year, month); } static List<int> getExpiryDate(String value) { var split = value.split(RegExp(r'(/)')); return [int.parse(split[0]), int.parse(split[1])]; } static bool hasMonthPassed(int year, int month) { var now = DateTime.now(); // The month has passed if: // 1. The year is in the past. In that case, we just assume that the month // has passed // 2. Card's month (plus another month) is more than current month. return hasYearPassed(year) || convertYearTo4Digits(year) == now.year && (month < now.month + 1); } static bool hasYearPassed(int year) { int fourDigitsYear = convertYearTo4Digits(year); var now = DateTime.now(); // The year has passed if the year we are currently is more than card's // year return fourDigitsYear < now.year; }
Aqui está o código-fonte, se quiser dar uma olhada nele.