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.
Top comments (0)