Como identificar o emissor do cartão de crédito e validar o número utilizando o Framework Flutter
gepdm
Posted on July 28, 2021
Olá, dev!
Neste tutorial, vamos desenvolver uma aplicação para validação do emissor e do número do cartão de crédito utilizando a linguagem Dart e o Framework Flutter.
Tópicos
Pré-requisitos
Instalação do Dart, Flutter e IDE (Integrated Development Environment)
Para começar, caso não tenha o Dart e o Flutter instalados, siga as instruções na documentação para prosseguir. Você também precisa de um editor de código ou IDE, nesse tutorial utilizaremos o Visual Studio Code.
Visão geral
Uma funcionalidade comum nos sistemas de pagamentos é a identificação do emissor e a validação do cartão de crédito ou débito. Para fazer isso, existem alguns algoritmos que facilitam a validação do cartão do lado do cliente.
O algoritmo que iremos utilizar neste tutorial, é o Algoritmo de Luhn.
Criando o projeto
Vamos iniciar, abra o seu terminal e crie um novo projeto com o Flutter command-line tool, substitua "my_app" por qualquer nome que queira dar ao aplicativo:
$ flutter create my_app
Entre no diretório e abra-o com o seu editor de código ou IDE preferida.
$ cd my_app
$ code .
Nossa estrutura final do projeto ficará semelhante a isso. Se você começou criando o projeto pelo Flutter cli, você não terá o diretório pages
e assets
, além dos arquivos abaixo deles por enquanto, veremos isso mais a frente.
.
├── android/
├── build/
├── ios/
├── lib/
│ ├── assets/
│ │ └── images/
│ │ ├── amex.png
│ │ ├── mastercard.png
│ │ └── visa.png
│ ├── pages/
│ │ └── card_page.dart
│ └── main.dart
├── test/
├── web/
├── .gitignore
├── .metadata
├── .packages
├── my_app.iml
├── README.md
└── pubspec.yaml
No diretório lib
, vamos até main.dart
, e em seguida remova o código desnecessário e deixe apenas a função main
e a classe MyApp
.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text("CardValidator"),
),
),
);
}
}
Agora, crie um diretório pages
dentro do diretório lib
e adicione um novo arquivo com o nome card_page.dart
. Feito isso, importe a biblioteca de UI/widgets do Flutter material.dart
e crie um widget de estado para que o componente _CardPageState
possa guardar informações.
Aqui vale um adendo. O que são widgets sem estado (StatelessWidget) e widgets de estado (StatefulWidget) ? Quando você tem um widget que nunca muda suas informações, ele não precisa ter estado e, logo, pode ser StatelessWidget. No caso de widgets que mudam informações quando o usuário interage com ele, esse widget precisa ser dinâmico, ou seja, ele pode mudar a aparência ou dados de acordo com eventos, dessa forma, você precisa de um StatefulWidget.
import 'package:flutter/material.dart';
class CardPage extends StatefulWidget {
@override
_CardPageState createState() => _CardPageState();
}
class _CardPageState extends State<CardPage> {
@override
Widget build(BuildContext context) {
}
}
Em seguida, vamos adicionar um formulário (Form
), uma estrutura básica de layout do material (Scaffold
), vamos adicionar o widget SafeArea
para evitar transbordamento do layout para outras partes do sistema operacional e uma Scroll View
para rolar a tela.
class _CardPageState extends State<CardPage> {
@override
Widget build(BuildContext context) {
return Form(
child: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
),
),
),
);
}
}
Agora que temos a estrutura básica pronta, volte para a classe MyApp
, importe o arquivo card_page.dart
e troque a home do MaterialApp
para a classe CardPage
.
class MyApp extends StatelessWidget {
/* ... */
return MaterialApp(
title: 'CardValidator',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CardPage(),
);
/* ... */
}
Continuando com o _CardPageState
, adicione um preenchimento (Padding
) para cima, vamos centralizar com o widget (Center
) e colocar uma caixa com um tamanho especificado (SizedBox
) e, por último, vamos adicionar uma coluna (Column
).
class _CardPageState extends State<CardPage> {
/* ... */
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(top: 50),
child: Center(
child: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center
),
),
),
),
),
/* ... */
}
O widget Column
aceita um array de widgets, iremos adicionar como primeiro widget do array o widget AspectRatio
e uma pilha de outros filhos como segundo filho do Container
, o qual irá criar nosso card.
/* ... */
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center
children: [
AspectRatio(
aspectRatio: 1.586,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 8,
blurRadius: 100
)
],
),
child: Stack(
children: [
],
),
),
),
],
),
/* ... */
Como filhos do widget Stack
, adicionaremos dois widgets Positioned
, para indicar onde eles serão posicionados dentro do card.
/* ... */
child: Stack(
children: [
Positioned(
top: 90,
left: 20,
child: Text(
"XXXX XXXX XXXX XXXX",
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w600
),
),
),
Positioned(
top: 20,
right: 20,
child: Text("Bandeira"),
)
],
),
/* ... */
Agora, vamos criar o input de texto, instale as dependências que iremos utilizar e importe no arquivo card_page.dart
.
Com o terminal, instale a dependência de máscara para o número do cartão.
flutter pub add easy_mask
Importe no arquivo card_page.dart
import 'package:flutter/services.dart';
import 'package:easy_mask/easy_mask.dart';
/* ... */
O segundo filho do widget Column
, será o input, vamos adicionar um padding do cartão e formatar o tamanho do input e label.
/* ... */
child: Column(
children: [
AspectRatio(/* ... */),
Padding(
padding: EdgeInsets.only(top: 40),
child: TextFormField(
keyboardType: TextInputType.number,
inputFormatters: [
TextInputMask(mask: '9999 9999 9999 9999')
],
onChanged: (_) {
setState(() {});
},
obscureText: false,
decoration: InputDecoration(
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.green,
width: 1
)
),
labelText: 'Número',
hintText: 'XXXX XXXX XXXX XXXX',
errorStyle: TextStyle(
color: Colors.green
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.green,
width: 1
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF646464),
width: 1
),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFF646464),
width: 1,
),
borderRadius: BorderRadius.circular(5),
),
contentPadding: EdgeInsets.symmetric(
vertical: 25,
horizontal: 15
),
),
),
),
)
/* ... */
Por último, vamos criar o botão de validação. Por enquanto ele não irá fazer nada, dado que nenhum estado foi alterado. Em seguida iremos criar as funções que serão disparadas ao evento de toque.
/* ... */
child: Column(
children: [
AspectRatio(/* ... */),
Padding(/* ... */),
Padding(
padding: const EdgeInsets.only(top: 25),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.lightBlue[900],
minimumSize: Size(120, 40),
),
onPressed: () {},
child: Text(
'Validar',
style: TextStyle(fontSize: 15),
),
),
],
),
),
],
),
/* ... */
Algoritmo de Luhn
O algoritmo Luhn é utilizado para validar uma variedade de números de identificação, como números de cartão de crédito que são aplicados a sites de comércio eletrônico, por exemplo. O número de cartão parece ser aleatório, mas cada parte tem um significado, eles são divididos em grupos responsáveis por identificar dados importantes do emissor, da indústria e da conta.
Os grupos são:
- Identificador principal da indústria (MII - Major Industry Identifier)
- Número de identificação do emissor (IIN - Issuer Identification Number)
- Número da conta
- Soma de verificação
Assim, o Algoritmo de Luhn determina a validade de um número de cartão utilizando o número da conta e a soma de verificação. Os números de posição par devem ser multiplicados por dois; caso o resultado dessa multiplicação seja maior que 9, devemos somar os dígitos (16 => 1+6=7), posteriormente somar todos os valores. Assim, a soma com o resto da divisão por 10 sendo igual a zero, o número do cartão será válido, caso contrário, será inválido.
Fonte: Laboratoria
Implementação do Algoritmo de Luhn
Bom, sabendo da teoria vamos ao código. Acima da classe CardPage
, crie uma função que recebe como parâmetro o número do cartão e retorna um resultado booleano (true/false), caso seja válido ou inválido.
bool isCardValid(String cardNumber) {
int sum = 0;
if (cardNumber != null && cardNumber.length >= 13) {
List<String> card = cardNumber.replaceAll(new RegExp(r"\s+"), "").split("");
int i = 0;
card.reversed.forEach((num) {
int digit = int.parse(num);
i.isEven
? sum += digit
: digit >= 5
? sum += (digit * 2) - 9
: sum += digit * 2;
i++;
});
}
return sum % 10 == 0 && sum != 0;
}
Agora, precisamos criar os estados (states) para guardar os dados de input e validação. O TextEditingController
vai notificar seus ouvintes para que possam ler o valor do texto toda vez que for editado. Vamos criar uma key
para o formulário, um widget de bandeira para ser trocado após a identificação do emissor e, por último, um booleano para identificar se o número é válido ou inválido, esse vai começar como false
.
Crie um método initState
, esse método irá ser chamado apenas uma vez quando o widget é inserido na ávore de widgets do Flutter, então atribua ao textController
o TextEditingController
.
class _CardPageState extends State<CardPage> {
TextEditingController textController = TextEditingController();
final _formKey = GlobalKey<FormState>();
Widget bandeira = Container();
bool isValid = false;
@override
void initState() {
super.initState();
textController = TextEditingController();
}
Widget build(BuildContext context) { /* ... */ }
Para colocar a bandeira do emissor, precisamos das imagens. Dessa forma, é necessário que você tenha as imagens em um diretório local e importe pelo path
relativo. Também, é preciso adicionar ao arquivo pubspec.yaml
o path relativo das imagens na lista assets
. Você pode baixar as imagens por aqui.
flutter:
assets:
- lib/assets/images/amex.png
- lib/assets/images/mastercard.png
- lib/assets/images/visa.png
Vamos criar a função que valida o emissor do cartão e nos retorne um widget da imagem correspondente. Vamos criar uma função assíncrona checkCardBanner
que recebe o número do cartão e retorna um widget, utilizaremos uma máscara de regex correspondente ao padrão que o emissor utiliza em seus cartões para validar.
Future<Widget> checkCardBanner(String card) async {
card = card.replaceAll(new RegExp(r"\s+"), "");
if (RegExp(r'^4\d{12}(\d{3})?$').hasMatch(card))
return Image.asset('lib/assets/images/visa.png', height: 30);
if (RegExp(r'^5[1-5]\d{14}$').hasMatch(card))
return Image.asset('lib/assets/images/mastercard.png', height: 50);
if (RegExp(r'^3[47]\d{13}$').hasMatch(card))
return Image.asset('lib/assets/images/amex.png', height: 60);
return Container();
}
Feito isso, vamos alterar algumas coisas para que os elementos mudem conforme os dados são modificados. Primeiro, adicione ao widget Form
da classe _CardPageState
a _formKey
que criamos acima.
/* ... */
return Form(
key: _formKey,
/* ... */
)
/* ... */
No input (TextFormField
) atribua a propriedade controller
o controlador de texto (textController
).
/* ... */
TextFormField(
controller: textController,
/* ... */
)
/* ... */
No onChanged
do botão, a cada modificação será disparado um evento que irá modificar o estado da variável bandeira
, vamos verificar o emissor passando o valor do input como argumento e na resposta da função assíncrona, vamos atribuir à variável bandeira
a imagem correspondente ao emissor.
/* ... */
onChanged: (_) {
setState(() {
checkCardBanner(textController.text).then(
(image) => bandeira = image);
});
/* ... */
No card, podemos mostrar o número do cartão conforme é alterado ou o padrão de formatação.
/* ... */
Positioned(
top: 90,
left: 20,
child: Text(
textController.text.isNotEmpty ?
textController.text :
'XXXX XXXX XXXX XXXX',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600),
),
),
/* ... */
Em seguida, para a bandeira do emissor aparecer, vá até o widget da bandeira e adicione como child
a variável bandeira
.
/* ... */
Positioned(
top: 20,
right: 20,
child: bandeira,
)
/* ... */
O input também aceita uma função validator
, aqui vamos validar o número de cartão chamando a função do algoritmo de Luhn e passar o valor do input, como resultado vamos obter um booleano. Adicione esse trecho de código antes ou após a função onChanged
do widget de input.
/* ... */
Padding(
child: TextFormField(
validator: (value) {
isValid = isCardValid(value);
return isValid
? 'Cartão válido'
: 'Cartão inválido';
},
),
),
/* ... */
Para dar feedback ao usuário sobre a validade ou não do input, precisamos verificar se o formulário foi preenchido. Podemos fazer isso adicionando um setState
ao onPressed
do botão. A partir da key
do Form
, conseguimos acessar os estados criados automaticamente pelo FormState
no montar o formulário e, assim, validar cada campo do formulário.
/* ... */
Padding(
child: Row(
ElevatedButton(
onPressed: () {
setState(() =>
_formKey.currentState.validate()
);
},
),
),
),
/* ... */
Ainda no input, vamos adicionar uma funcionalidade de limpar o input e algumas customizações. Quando o input não estiver vazio, vamos adicionar um ícone para limpar o campo de texto.
/* ... */
Padding(
child: TextFormField(
decoration: InputDecoration(
suffixIcon:
textController.text.isNotEmpty
? InkWell(
onTap: () => setState(() => textController.clear()),
child: Icon(
Icons.clear,
color: Color(0xFF757575),
size: 22,
),
)
: null,
)
),
),
/* ... */
Agora, vamos fazer uma condicional para trocar as cores das bordas do input para quando o número for válido ou inválido.
/* ... */
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isValid ? Colors.green : Colors.red,
)
)
/* ... */
Também, vamos alterar a cor na borda de erro do input.
/* ... */
decoration: InputDecoration(
errorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isValid ? Colors.green : Colors.red,
),
),
),
/* ... */
Para finalizar, vamos alterar o estilo do input para quando o cartão for inválido.
/* ... */
decoration: InputDecoration(
errorStyle: TextStyle(
color: isValid ? Colors.green : Colors.red,
),
),
/* ... */
Finalizamos a aplicação, para ver o código completo clique aqui. Para fazer build
execute no terminal o comando a seguir.
flutter build apk # android
flutter build ios # apple
Muito obrigado por ler! 👋
Referências
Hussein, Khalid Waleed, et al. Enhance Luhn Algorithm for Validation of Credit Cards Numbers. 2013.
Posted on July 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.