Checkout Transparente da Web no Android
(12480) (23)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos falar, e praticar, sobre pagamentos em aplicativos Android utilizando o conhecido modelo: Checkout Transparente.
Vale ressaltar já aqui que apesar de existirem inúmeras empresas para processamento de pagamentos em ambientes digitais, são poucas as que têm uma API dedicada somente ao ambiente Android.
Confesso que até a época da construção deste artigo nenhuma das empresas mais populares fornecia uma API de checkout transparente, estável, para apps que processam pagamentos no Brasil.
O objetivo deste conteúdo é apresentar uma implementação de checkout transparente no Android, com APIs Web, de maneira que você consiga utilizar o mesmo roteiro para qualquer software Web de pagamento que você tenha escolhido para rodar também no ambiente mobile.
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber todos os conteúdos de desenvolvimento Android exclusivos aqui do Blog, além de recebe-los também em versão PDF (somente por lá).
A seguir os tópicos abordados no artigo:
- Modelo comum de checkout transparente;
- Por que utilizar também em sistemas mobile?;
- Construindo o projeto de pagamento;
- Vídeo com implementação passo a passo;
- Conclusão.
Modelo comum de checkout transparente
As empresas que fornecem o modelo de pagamento onde os usuários de nosso software Web não precisam sair do site para pagar as compras, geralmente essas empresas compartilham o mesmo modelo de checkout transparente, como o da ilustração abaixo:
Esses sistemas liberam uma API front-end em JavaScript onde a parte front-end de nossos sistemas Web é responsável por coletar os dados de cartão de crédito do usuário e então utilizar métodos dessa API JavaScript de Pagamento para enviar esses dados aos servidores deles, que consequentemente, caso aprovado, retornam um token representando esses dados de cartão de crédito.
Esse token somente pode ser utilizado uma vez, logo, então novos tokens devem ser gerados para novos pagamentos, mesmo sendo o mesmo usuário e cartão.
Recebido o token, devemos enviá-lo, agora ao nosso back-end Web, juntamente com alguns dados que identifiquem os produtos em compra.
Logo depois novamente utilizamos a API de pagamento, porém agora no back-end. Nessa parte o pagamento é que será validado (anteriormente foram os dados de cartão apenas), caso aprovado recebemos esse status (paid, por exemplo), caso contrário recebemos o status referente a rejeição e o porquê dessa.
Como informado: esse é o modelo mais comumente adotado pelas empresas de pagamento online quando o funcionalidade é a de checkout transparente.
Por que isso, digo, o passo no front-end? Por que não diretamente enviar os dados de cartão para nosso back-end e utilizá-los somente com a versão back-end de API de pagamento?
Enviar os dados de cartão pela rede para seu back-end quando você não passou pelos testes do PCI-DSS é, teoricamente, um "crime".
Pois há vários itens de segurança que devem ser tratados antes que seu sistema possa realizar esse envio direto de dados de cartão.
Lembre-se de que mesmo utilizando criptografia na camada de aplicação (HTTPS), os dados que saem da máquina do usuário e trafegam para seu servidor, esses dados passam antes por vários outros computadores.
Uma das possíveis penalidades, caso mesmo sabendo dos problemas você envie os dados de cartão para seu back-end Web, é o "boicote" ao seu e-commerce ou sistema de vendas Web ou mobile.
Como?
As empresas de cartão de crédito (VISA, MasterCard, Dinners, ...) começam a negar os pagamentos que vem de seu sistema.
Porém utilizando uma empresa de pagamentos online quem será punido é ela, pois são os servidores dela que se comunicam com os servidores das empresas de pagamento.
Logo, muito provavelmente você não encontrará uma dessas empresas de pagamentos online que permita que você faça isso: utilize a API back-end com dados de cartão de crédito.
Por que utilizar também em sistemas mobile?
Existem vários pontos, abaixo listo alguns:
- Manter o mesmo sistema de pagamento já utilizado em seus sistemas Web. Isso quando o sistema de pagamento não oferece uma API estável para pagamentos no Android;
- Evitar pagar taxas muito altas em transações mobile. O Android APP e o In-Billing APP, por exemplo, ficam com nada mais nada menos que 30% do valor de venda, ao menos o In-Billing, enquanto sistemas Web de pagamento ficam com aproximadamente 5%;
- Utilizar o mesmo sistema de pagamento Web no ambiente mobile, isso para que os clientes se sintam seguros em continuar comprando em ambos os ambientes;
- Devido a facilidade de integração. O modelo que vamos utilizar aqui é provavelmente mais simples do que algumas APIs de pagamento disponíveis para Android.
Construindo o projeto de pagamento
Agora podemos prosseguir com nosso projeto de pagamento, um pequeno, mas completo projeto que aborda o necessário para rodar os métodos de pagamentos Web no Android, com código nativo.
Aqui vou utilizar o sistema da Pagar.me, porém você pode utilizar o que achar melhor, existem vários. Lembrando que o modelo de checkout transparente quase sempre é o mesmo.
Para auxílio ao mini projeto de pagamento vamos utilizar também:
- Library para conexão Web, Retrofit 2;
- Entrada de texto com EditText e TextInputLayout;
- Dialog com a library MDDialog;
- WebView e integração WebView com JavaScript;
- runOnUiThread;
- Padrão de projeto Observer.
Com isso vamos seguir com o código.
Nosso primeiro passo é atualizar o Gradle APP level, buid:gradle (Module: app), para já incluir as dependências necessárias:
...
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.0'
compile 'com.android.support:design:24.2.0'
testCompile 'junit:junit:4.12'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.google.code.gson:gson:2.7'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'cn.carbs.android:MDDialog:1.0.0'
}
...
As três primeiras dependências marcadas, negrito, são para o Retrofit e Gson, a última é para o MDDialog.
No AndroidManifest.xml adicione a permissão de Internet:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pagamentosapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Em nosso domínio do problema vamos ter duas classes, Product e CreditCard. Segue os códigos de Product:
public class Product {
private String id;
private String name;
private String description;
private int stock;
private double price;
private int img;
public Product( String ident, String n, String d, int s, double p, int i ){
id = ident;
name = n;
description = d;
stock = s;
price = p;
img = i;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public String getStockString() {
return "Apenas "+String.valueOf(stock)+" no estoque.";
}
public double getPrice() {
return price;
}
public String getPriceString() {
return "R$ "+String.valueOf(price).replace('.', ',');
}
public int getImg() {
return img;
}
}
Nada de novo, apenas uma classe POJO (atributos e métodos de atualização e acesso aos valores desses atributos). Agora a classe CreditCard:
public class CreditCard {
private String cardNumber;
private String name;
private String month;
private String year;
private String cvv;
private int parcels;
private String error;
private String token;
public CreditCard(Observer observer){
addObserver( observer );
}
public String getCardNumber() {
return cardNumber;
}
public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getCvv() {
return cvv;
}
public void setCvv(String cvv) {
this.cvv = cvv;
}
public int getParcels() {
return parcels;
}
public void setParcels(int parcels) {
this.parcels = parcels;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
Como em Product, outro POJO.
Com isso podemos prosseguir com os códigos de layout. Começando com o layout da MainActivity. Esse está dividido em duas partes, content_main.xml e activity_main.xml. Começando pelo layout activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="#fff"
tools:context="br.com.thiengo.pagamentosapp.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
</android.support.design.widget.CoordinatorLayout>
E então o layout referenciado dentro de activity_main.xml, content_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="br.com.thiengo.pagamentosapp.MainActivity"
tools:showIn="@layout/activity_main">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/img"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginRight="16dp"
android:scaleType="centerCrop"
android:src="@mipmap/tennis" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/img"
android:layout_toRightOf="@+id/img"
android:textColor="#212121"
android:textSize="24sp" />
<TextView
android:id="@+id/price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/name"
android:layout_toRightOf="@+id/img"
android:textColor="#f00"
android:textSize="22sp" />
<TextView
android:id="@+id/stock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/price"
android:layout_toRightOf="@+id/img"
android:textColor="#555"
android:textSize="18sp" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/stock"
android:layout_marginTop="16dp"
android:textColor="#212121"
android:textSize="16sp" />
<Button
android:id="@+id/button_buy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#ff5100"
android:gravity="center"
android:padding="10dp"
android:text="@string/button_buy"
android:textColor="#fff" />
</RelativeLayout>
</ScrollView>
Com isso, dois dos três principais layouts XML já foram construídos. O Terceiro layout é referente ao pagamento. Esse será apresentado dentro de um dialog para que não seja necessária a troca de Activity para finalizar a compra.
Vamos agora aos códigos de payment.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:padding="10dp">
<LinearLayout
android:id="@+id/ll_card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:orientation="horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginRight="5dp"
android:layout_weight="0.4"
android:contentDescription="Cartão VISA"
android:src="@mipmap/visa" />
<ImageView
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="0.4"
android:contentDescription="Cartão Master Card"
android:src="@mipmap/master_card" />
<android.support.design.widget.TextInputLayout
android:id="@+id/til_card_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1.2">
<EditText
android:id="@+id/card_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Número do cartão"
android:inputType="number"
android:text="4485203648101323" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_card_number"
android:layout_marginTop="10dp">
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nome do proprietário cartão"
android:text="Thiengo Calopsita" />
</android.support.design.widget.TextInputLayout>
<LinearLayout
android:id="@+id/ll_expiration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/til_name"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_month"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/month"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Mês"
android:inputType="number"
android:text="01" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_year"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1">
<EditText
android:id="@+id/year"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Ano"
android:inputType="number"
android:text="2023" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_expiration"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<android.support.design.widget.TextInputLayout
android:id="@+id/til_parcels"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_weight="1">
<EditText
android:id="@+id/parcels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Parcelas"
android:inputType="number"
android:text="2" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_cvv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<EditText
android:id="@+id/cvv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="CVV"
android:inputType="number"
android:text="253" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
</RelativeLayout>
O layout é realmente um pouco grande, isso para conter todos os campos obrigatórios, ao menos os de cartão de crédito. Para um formulário mais completo, você pode querer colocar dados de endereço também.
Caso esteja programando para vender infoprodutos, não há tanta necessidade em trabalhar com validação de dados por meio de empresas especializadas (quando você precisa dos dados de endereço do usuário).
Com infoprodutos você pode utilizar o pagamento de forma síncrona. Isso pois cada campo a mais no formulário tende a diminuir o número de conversão em seu sistema (Otimização da Página de Entrada).
Se estiver vendendo produtos físicos, a validação do pagamento incluindo endereço e outros dados, se faz necessária, caso contrário, depois do chargeback você pode perder muito dinheiro, pois o produto foi entregue e o pagamento estornado.
Note que pagamentos com validação por meio de empresas especializadas utilizam sistema assíncrono, onde Web hooks serão utilizados. Sistema assíncrono é inevitável também com boletos bancários.
Com isso podemos seguir com o código, agora da MainActivity:
public class MainActivity extends AppCompatActivity {
private Product product;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
initProduct();
initViews( product );
}
private void initProduct(){
product = new Product(
"6658-3324599755412",
"TÊNIS ADIDAS BARRICADE COURT 2",
"Adiwear: Borracha de altíssima durabilidade que permite que a sola não marque o solo./ Adiprene +: Protege a parte dianteira do pé proporcionando./ Adiprene: Proporciona alta absorção de impactos para amortecer e proteger o calcanhar.",
3,
69.90,
R.mipmap.tennis);
}
private void initViews( Product product ){
((ImageView) findViewById(R.id.img)).setImageResource( product.getImg() );
((TextView) findViewById(R.id.name)).setText( product.getName() );
((TextView) findViewById(R.id.description)).setText( product.getDescription() );
((TextView) findViewById(R.id.stock)).setText( product.getStockString() );
((TextView) findViewById(R.id.price)).setText( product.getPriceString() );
}
}
Nada de novo até aqui. Um objeto do tipo Product sendo inicializado e logo depois os dados dele sendo utilizados para preencher as Views em content_main.xml.
Agora vamos adicionar um listener de clique para o Button presente em content_main.xml. Vamos adicionar a referência diretamente no XML:
...
<Button
android:id="@+id/button_buy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#ff5100"
android:gravity="center"
android:onClick="buy"
android:padding="10dp"
android:text="@string/button_buy"
android:textColor="#fff" />
...
Agora o método buy() em MainActivity:
...
public void buy( View view ){
new MDDialog.Builder(this)
.setTitle("Pagamento")
.setContentView(R.layout.payment)
.setNegativeButton("Cancelar", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
})
.setPositiveButton("Finalizar", new View.OnClickListener() {
@Override
public void onClick(View v) {
View root = v.getRootView();
CreditCard creditCard = new CreditCard( MainActivity.this );
creditCard.setCardNumber( getViewContent( root, R.id.card_number ) );
creditCard.setName( getViewContent( root, R.id.name ) );
creditCard.setMonth( getViewContent( root, R.id.month ) );
creditCard.setYear( getViewContent( root, R.id.year ) );
creditCard.setCvv( getViewContent( root, R.id.cvv ) );
creditCard.setParcels( Integer.parseInt( getViewContent( root, R.id.parcels ) ) );
getPaymentToken( creditCard );
}
})
.create()
.show();
}
...
Adicionalmente já implementamos o conteúdo de onClick() e do setPositiveButton() de MDDialog. Já inicializamos um objeto CreditCard que contém, depois do clique em "Finalizar", os dados dos campos do layout payment.xml.
O método getPaymentToken() é referente ao envio de dados do Android, os dados preenchidos no dialog de pagamento, para o JavaScript de uma página que vamos criar para trabalhar com a API front-end do sistema de pagamento.
Antes de partirmos para a construção desse método, vamos primeiro criar o folder assets caso ele ainda não exista em seu projeto.
Primeiro altere a visualização do projeto Android para Project (o padrão é Android):
Logo depois clique em (ou expanda) app, logo depois em src e assim em main. Clique com o botão direito em cima de main e então em New, logo depois clique em Directory. Coloque o nome assets e então clique em Ok:
Agora clique com o botão direito do mouse em assets, logo depois em New e então clique em File. Preencha o campo nome com index.html:
Assim pode voltar ao modo de visualização Android.
Expanda assets folder, abra o arquivo index.html e coloque nele o HTML necessário para utilizar a versão front-end da API de pagamento que você escolheu.
A empresa de pagamento que você utiliza tem de ter na documentação dele os scripts JavaScript para integrar ao seu sistema.
Abaixo o HTML que utilizo em index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://assets.pagar.me/js/pagarme.min.js"></script>
</head>
<body>
<script>
$(document).ready(function() {
PagarMe.encryption_key = "sua api key";
var creditCard = new PagarMe.creditCard();
creditCard.cardHolderName = Android.getName();
creditCard.cardExpirationMonth = Android.getMonth();
creditCard.cardExpirationYear = Android.getYear();
creditCard.cardNumber = Android.getCardNumber();
creditCard.cardCVV = Android.getCvv();
var fieldErrors = creditCard.fieldErrors();
var errors = [], i = 0;
for(var field in fieldErrors) { errors[i++] = field; }
if(errors.length > 0) {
Android.setError( errors );
} else {
/* se não há erros, gera o card_hash... */
creditCard.generateHash(function(cardHash) {
Android.setToken( cardHash );
});
}
});
</script>
</body>
</html>
As duas primeiras tags em <head> são referentes a library do jQuery no CDN do Google (lista de libraries no CDN do Google). E então a referente a API de pagamento que utilizo aqui.
No script JavaScript dentro de <body> estou com a chave de testes. Pode ser que o sistema de pagamentos que você utiliza libere uma versão sandbox, onde as chamadas de métodos é que tendem a ser diferentes do modo em produção.
Em sandbox até mesmo a url da API front-end pode ser diferente da url em modo de produção. Adapte o código aqui de acordo com o código que você tem de utilizar no front-end de seu sistema.
Note que a entidade Android é referente a vinculação de interface que temos de realizar nos códigos nativos do Android. Vinculação entre WebView e JavaScript.
Todo o restante é referente a lógica de negócio que utilizei junto a API de pagamento, para melhor atender ao meu domínio do problema no APP Android. O algoritmo que realmente nos interessa, digo, o retorno dele, é:
...
creditCard.generateHash(function(cardHash) {
Android.setToken( cardHash );
});
...
O script acima é responsável por enviar o token de pagamento, gerado no front-end, para nosso código Android.
Em content_main.xml (tem que ser o layout do produto e não o do dialog) vamos adicionar o XML do WebView, logo abaixo do Button de pagamento:
...
<WebView
android:id="@+id/web_view"
android:layout_width="0.1dp"
android:layout_height="0.1dp"
android:layout_below="@+id/button_buy"></WebView>
...
Agora vamos colocar o conteúdo do método getPaymentToken(), em MainActivity:
private void getPaymentToken( CreditCard creditCard ){
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled( true );
webView.addJavascriptInterface( creditCard, "Android" );
webView.loadUrl("file:///android_asset/index.html");
}
Note como é que referenciamos um arquivo no folder assets. Note também que em addJavaScriptInterface() estamos utilizando, além do label "Android" que é utilizado no JavaScript, um objeto do tipo CreditCard. Mais precisamente o enviado como argumento a partir do onClick() de setPositiveButton() do objeto de dialog, MDDialog.
A classe CreditCard precisa ser atualizada para que os métodos invocados dela no código JavaScript possam funcionar. Para isso devemos colocar @JavascriptInterface nesses métodos:
public class CreditCard {
...
@JavascriptInterface
public String getCardNumber() {
return cardNumber;
}
...
@JavascriptInterface
public String getName() {
return name;
}
...
@JavascriptInterface
public String getMonth() {
return month;
}
...
@JavascriptInterface
public String getYear() {
return year;
}
...
@JavascriptInterface
public String getCvv() {
return cvv;
}
...
@JavascriptInterface
public void setError(String... errors) {
for( String e : errors ){
if( e.equalsIgnoreCase("card_number") ){
error += "Número do cartão, inválido; ";
}
}
}
...
@JavascriptInterface
public void setToken(String token) {
this.token = token;
Log.i("log", "Token: " + token); /* PARA VERIFICAR CRIAÇÃO DE TOKEN */
}
}
Veja que também atualizamos o método setError(). Isso para que ele trabalhe com o array de erros que pode ser enviado do JavaScript. Com isso, o código JavaScript que foi apresentado anteriormente já é todo funcional.
Rodando o projeto, temos:
Logo depois, clicando em "Comprar" e então em "Finalizar", temos nos logs do Android Studio que o token está sendo gerado e enviado ao nosso objeto creditCard:
...
...I/log: token: 185468_HWHGF6eWwkn79HDv3H0AsZZhuvbehUWFj3BEJDAyCgFoPrN6N57Ya4weBdTRz8llXJ...
...
Show de bola! Agora precisamos enviar esse token e mais alguns dados de produto ao nosso back-end, para que o pagamento seja processado.
Primeiro vamos aplicar o padrão de projeto Observer em nossa classe CreditCard. Ela será a entidade observada por outros objetos que precisam dos dados dela. Mais precisamente do token e da mensagem de erro.
Vamos utilizar as entidades nativas do Java para trabalhar com o padrão Observer, logo atualize os seguintes códigos em CreditCard:
public class CreditCard extends Observable {
...
public CreditCard(Observer observer){
addObserver( observer );
}
...
@JavascriptInterface
public void setError(String... errors) {
for( String e : errors ){
if( e.equalsIgnoreCase("card_number") ){
error += "Número do cartão, inválido; ";
}
/* TODO */
}
Log.i("log", "error: "+error);
setChanged();
notifyObservers();
}
...
@JavascriptInterface
public void setToken(String token) {
this.token = token;
Log.i("log", "Token: "+token);
setChanged();
notifyObservers();
}
}
Note que agora temos um construtor onde devemos vincular as entidades observadoras do padrão a nossa entidade observada, ou seja, nossos Observers em nossa classe Subject.
Entre no post do padrão indicado acima para entender como ele funciona, é bem simples.
Dois dados, quando são atualizados, são gatilhos de notificação as classes observadoras, são eles: token e erro. Logo, somente nos métodos de atualização dessas variáveis é que temos as chamadas abaixo:
...
setChanged();
notifyObservers();
...
Ok, você deve estar se perguntando: quais serão as entidade observadoras?
Na verdade somente uma. A instância da MainActivity, pois os códigos de conexão, que precisam de token ou erro, vão estar em um método nessa Activity.
Assim atualizamos a assinatura dessa classe para implementar o a Interface Update. Consequentemente atualizamos a instanciação de CreditCard em onClick() de setPositiveButton():
public class MainActivity extends AppCompatActivity implements Observer {
...
public void buy( View view ){
new MDDialog.Builder(this)
.setTitle("Pagamento")
.setContentView(R.layout.payment)
.setNegativeButton("Cancelar", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
})
.setPositiveButton("Finalizar", new View.OnClickListener() {
@Override
public void onClick(View v) {
View root = v.getRootView();
CreditCard creditCard = new CreditCard( MainActivity.this );
...
}
})
.create()
.show();
}
private String getViewContent( View root, int id ){
EditText field = (EditText) root.findViewById(id);
return field.getText().toString();
}
private void getPaymentToken( CreditCard creditCard ){
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled( true );
webView.addJavascriptInterface( creditCard, "Android" );
webView.loadUrl("file:///android_asset/index.html");
}
@Override
public void update(Observable o, Object arg) {
/* TODO */
}
}
Antes de prosseguir com o conteúdo do método sobrescrito, update(), vamos ter de criar uma Interface para que seja possível trabalhar com o Retrofit. Segue código de PaymentConnection:
public interface PaymentConnection {
@FormUrlEncoded
@POST("package/ctrl/CtrlPayment.php")
public Call<String> sendPayment(
@Field("product_id") String id,
@Field("value") double value,
@Field("token") String token,
@Field("parcels") int parcels
);
}
Vou assumir que o código acima não é uma espécie de "pergaminho criptografado" para você, pois já conhece o conteúdo sobre a Retrofit API.
Agora podemos prosseguir com o conteúdo do método update() na MainActivity:
...
@Override
public void update(Observable o, Object arg) {
CreditCard creditCard = (CreditCard) o;
/* CLÁUSULA DE GUARDA */
if( creditCard.getToken() == null ){
buttonBuying( false );
showMessage( creditCard.getError() );
return;
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://192.168.25.221:8888/android-payment/")
.addConverterFactory( GsonConverterFactory.create() )
.build();
PaymentConnection paymentConnection = retrofit.create(PaymentConnection.class);
Call<String> requester = paymentConnection.sendPayment(
product.getId(),
product.getPrice(),
creditCard.getToken(),
creditCard.getParcels()
);
requester.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
buttonBuying( false );
showMessage( response.body() );
}
@Override
public void onFailure(Call<String> call, Throwable t) {
buttonBuying( false );
Log.e("log", "Error: "+t.getMessage());
}
});
}
...
No código acima, sabemos que ele é acionado somente quando há uma atualização em token ou erro nas instâncias de CreditCard. Logo, nosso Observable é na verdade nossa instância de CreditCard.
Antes de prosseguir com o envio de dados para o back-end Web, utilizamos o padrão Cláusula de Guarda para garantir se os dados de envio, mais precisamente o token que é gerado dinamicamente, estão todos presentes, caso não, mudamos o label do Button de compra para "Comprar" novamente, além de apresentar a mensagem de erro.
Caso tudo ok, ou seja, o token está presente. Inicializamos as configurações do Retrofit e realizamos o envio.
Note que aqui nosso projeto Web está localhost, por isso o IP local de minha máquina na url de conexão.
Vamos prosseguir com a implementação dos métodos referenciados em update(), buttonBuying() e showMessage():
...
private void showMessage( final String message ){
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText( MainActivity.this, message, Toast.LENGTH_LONG ).show();
}
});
}
...
private void buttonBuying( final boolean status ){
runOnUiThread(new Runnable() {
@Override
public void run() {
String label;
label = getResources().getString(R.string.button_buy);
if( status ){
label = getResources().getString(R.string.button_buying);
}
((Button) findViewById(R.id.button_buy)).setText(label);
}
});
}
...
showMessage() dispensa comentários, pois faz exatamente o que o nome indica.
buttonBuying() altera o label do Button de compra. Quando o usuário clica em "Finalizar" no dialog de pagamento, o label desse Button altera para "Processando pagamento...", ou seja, o parâmetro status é true.
Caso contrário o label volta ao valor padrão, "Comprar", quando status é false.
Muito provavelmente você deve estar se perguntando: por que a utilização do runOnUiThread()?
Esse código está ali devido as chamadas em update() aos métodos, showMessage() e buttonBuying().
Mais precisamente quando as invocações a esses métodos não são realizadas dentro dos métodos de resposta do retrofit, onResponse() e onFailure() (dentro dos métodos de retrofit é garantida que a Thread sendo utilizada é a Thread principal).
Note que o método update() somente é chamado quando há chamadas aos métodos setToken() ou setError(). Em nosso caso essas chamadas somente ocorrerão no código JavaScript em nossa WebView.
Qual o problema quanto a isso?
Essas execuções em JavaScript ativam nosso objeto do tipo CreditCard, porém fora da Thread principal, logo, atualizar qualquer View fora dessa Thread... nós já sabemos do resultado, crash!
O runOnUiThread() vai funcionar para chamadas dentro ou fora da Thread de UI.
Antes de prosseguir para o back-end para ver como fica nosso código, vamos colocar uma linha de código antes da instanciação de CreditCard, em onClick() de setPositiveButton(). Segue linha a ser a adicionada:
...
buttonBuying( true );
...
Isso, pois é nesse ponto que nosso Button de pagamento já deve ter o label dele atualizado.
Agora podemos prosseguir com o código back-end. Em me caso, o Pagar.me me oferece uma API em PHP, então é ela que utilizo. O objetivo aqui, do back-end, é somente um teste para ver se o pagamento passa sem problemas:
require("../util/pagarme-php/Pagarme.php");
/* API DE PAGAMENTO SENDO UTILIZADA NO BACK-END, COM O TOKEN AO INVÉS DE DADOS DE CARTÃO */
Pagarme::setApiKey("api key de testes");
$transaction = new PagarMe_Transaction(array(
'amount' => ($_POST['value'] * 100),
'card_hash' => $_POST['token']
));
$transaction->charge();
$status = $transaction->status;
/* OPCIONAL, PARA SABER DOS DDOS ENVIADOS A API DE PAGAMENTO */
$file = fopen('token.txt', 'w');
fwrite($file, $_POST['product_id']."\n");
fwrite($file, ($_POST['value'] * 100)."\n");
fwrite($file, $_POST['token']."\n");
fwrite($file, $_POST['parcels']."\n");
fwrite($file, $status."\n");
fclose($file);
/* MENSAGEM DE RETORNO AO ANDROID, O ANDROID ENTENDE COMO OBJETO JSON */
if( strcasecmp($status, 'refused') == 0 ){
echo '"Pagamento recusado. Tente outro cartão."';
}
else{
echo '"Pagamento aprovado. Em breve o produto estará em suas mãos."';
}
Note que se seu back-end for em Python, Java, Ruby, ... utilize-o como já utiliza para pagamentos Web, pode até mesmo reaproveitar os mesmos códigos, somente não esqueça de identificar quando o pagamento veio da interface mobile e quando da Web.
Abaixo o projeto sendo executado e com o Button "Finalizar" já pressionado:
Então acessando o Dashboard da empresa de pagamentos online, temos, em modo teste:
Com isso integramos o pagamento com o modelo checkout transparente ao nosso projeto Android, projeto em código com linguagem oficial, a linguagem Java.
Muito mais simples que você provavelmente deve ter imaginado que seria.
Antes de prosseguir para a conclusão, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber semanalmente os conteúdos exclusivos sobre desenvolvimento mobile.
Se inscreva também no canal do Blog em YouTube Thiengo.
Vídeo com a implementação passo a passo
Abaixo o vídeo com a implementação passo a passo do projeto proposto aqui. Ele é um pouco longo, mas é bem completo:
Para acessar o projeto versão Android entre no seguinte GitHub: https://github.com/viniciusthiengo/PagamentosAPP
Para acessar o versão Web entre em: https://github.com/viniciusthiengo/PagamentosAPP-web-version
Conclusão
As principais vantagens na utilização do checkout transparente, com APIs Web, em plataforma mobile estão em:
- Manter o mesmo meio de pagamento. Caso quando já utilizando a mesma empresa e API na versão Web do projeto;
- e Em manter as mesmas taxas de cobrança da empresa de pagamentos, independente do ambiente utilizado.
Note que o código, como apresentado aqui, não viola em nada as recomendações das empresas de pagamento, pois nós nos mantemos "não trabalhando" com dados de cartão de crédito em nosso back-end Web.
O ponto negativo no modelo de código apresentado em artigo, a princípio, está em quando o sistema de pagamento utilizado por você já oferece uma API Android que é estável e pode ser integrada mais facilmente.
Neste caso vale ao menos testar essa versão já pronta da API para Android.
Para layouts mais sofisticados para formulários de pagamento, acesse o conteúdo do link a seguir: Android Arsenal (CreditCard).
Caso você tenha dúvidas ou dicas, não deixe de comentar abaixo que logo eu lhe respondo.
Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, se inscreva na 📩 lista de e-mails, respondo às suas dúvidas também por lá.
Abraço.
Comentários Facebook