Checkout Transparente da Web no Android

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação. Você receberá um email de confirmação. Somente depois de confirmar é que poderei lhe enviar o conteúdo exclusivo por email.

Email inválido.
Blog /Android /Checkout Transparente da Web no Android

Checkout Transparente da Web no Android

Vinícius Thiengo12/09/2016, Segunda-feira, às 09h
(1323) (11) (61) (7)

Opa, blz?

Nesse artigo vamos falar sobre checkout transparente em APPs Android, que apesar de já existirem APIs de alguns sistemas de pagamentos para vendas mobile, ainda há muito software de pagamento, bem conhecido na Web, que não tem uma library estável para o Android.

O objetivo aqui é apresentar uma implementação de checkout transparente, no Android, de maneira que você consiga utilizar o mesmo conteúdo para qualquer software Web de pagamento que tenha escolhido para rodar também no ambiente mobile.

Abaixo os tópicos abordados no artigo:

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 frontend em JavaScript onde a parte frontend 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 backend Web, juntamente com alguns dados que identifiquem os produtos em compra.

Logo depois novamente utilizamos a API de pagamento, porém agora no backend. 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 frontend? Por que não diretamente enviar os dados de cartão para nosso backend e utilizá-los somente com a versão backend de API de pagamento?

Enviar os dados de cartão pela rede para seu backend quando você não passou pelos testes do PCI-DSS é, teóricamente, 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 backend 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 backend 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:

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 frontend 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 frontend 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 frontend 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 frontend 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 frontend, 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 AndroidStudio 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 backend, 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 Retrofit (indicado nesse post: Library Retrofit 2 no Android).

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 backend 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 implemenaçã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 backend 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 backend. Em me caso, o Pagar.me me oferece uma API em PHP, então é ela que utilizo. O objetivo aqui, do backend, é somente um teste para ver se o pagamento passa sem problemas:

require("../util/pagarme-php/Pagarme.php");

/* API DE PAGAMENTO SENDO UTILIZADA NO BACKEND, 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 backend 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 um checkout transparente ao nosso código Android nativo. Muito mais simples que você provavelmente deve ter imaginado que seria.

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, Web, em plataforma mobile está em: manter o mesmo meio de pagamento se já utilizando um na versão Web de sua APP; e em continuar com as taxas de pagamento já aceitas sendo cobradas por cada transação mobile e não taxas em 30%.

Note que o código, como apresentado aqui, não viola em nada as recomendações das empresas de pagamento, pois nós mantemos "não trabalhando" com dados de cartão de crédito em nosso backend Web.

O ponto negativo no modelo de código acima, 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. Nesse caso vale ao menos testar essa versão já pronta da API para Android.

Para layouts mais sofisticados para formulários de pagamento, acesse esse conteúdo: Android Arsenal (CreditCard)

Vlw.

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Relacionado

Monetização sem Anúncios utilizando a Huq SDKMonetização sem Anúncios utilizando a Huq SDKAndroid
As 33 Coisas que Todo Programador Deve Parar de FazerAs 33 Coisas que Todo Programador Deve Parar de FazerEmpreendedorismo
Padrão de Projeto: Template Method (Método Template)Padrão de Projeto: Template Method (Método Template)Android
ConstraintLayout, Melhor Performance no AndroidConstraintLayout, Melhor Performance no AndroidAndroid

Compartilhar

Comentários Facebook (8)

Comentários Blog (3)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Guihgo (1) (0)
12/09/2016, Segunda-feira, às 15h
em 4:00 min você mostrou o workflow do processo da compra. Nas últimas etapas (quando o sistema de pagamento retorna status da transição para o comprador) tem uma falha, porque se o sistema de pagamento de transição não consegui retorna o status do pagamento (nem se for um erro), a transição será completada ( debitada do cartão do comprador) no sistema de pagamento, mas não haverá confirmação do sucesso da transição para o usuário.   Assim haverá pagamento, mas o usuário não poderá ter o produto.
Responder
Vinícius Thiengo (1) (0)
13/09/2016, Terça-feira, às 01h
Guihgo, blz?
Nesse contexto, seu backend deve ser mais elaborado. No fluxo que apresentei somente tem o caminho perfeito. Esse caso é ponto fora da curva, mas de qualquer forma vc está certo, isso pode ocorrer. Nem mesmo um rollback nos servers de pagamento estaria a tempo de reverter a compra, pois os dados de feedback podem se perder na rede (como falei, ponto fora da curva).

Com essa problemática e um backend um pouco mais trabalhado, teria ao menos um envio de email, ou algo parecido, para vc dev do sistema, informando o problema e o id da compra. Assim é mudar o status da compra no bd de seu sistema e enviar / liberar o produto ou ir no dashboard da empresa de pagamento e aplicar o estorno.

Com isso deve resolver, mas terá trabalho manual. Abraço
Responder
13/09/2016, Terça-feira, às 15h
Interessante seu discurso. E ótima solução. Vou precisar repensa na estrutura do processo. Mas de qualquer forma, muito obrigado pela atenção.
Responder