Olá, pessoal! No artigo “Manipulador de notificação de pagamento instantâneo“, ilustramos como construir um manipulador para receber e validar as notificações instantâneas de pagamento do PayPal e como construir as regras de negócio para utilizar essa informação.
Já sabemos que as notificações de pagamento são assíncronas, ou seja, elas podem ocorrer a qualquer momento. Isso significa que, se precisarmos da confirmação da mudança de status de um determinado pagamento para tomar alguma decisão relacionada ao negócio, precisaremos monitor a aplicação para ver se uma notificação chegou, ou então fazer uma consulta manual, utilizando a operação GetTransactionDetails para saber se houve tal mudança de status de pagamento.
A ideia desse artigo é utilizar o manipulador descrito no artigo supracitado em conjunto com o serviço Android Cloud to Device Messaging (C2DM) para notificar dispositivos móveis sobre qualquer mudança de status de pagamento. Dessa forma, assim que uma mensagem IPN chegar, receberemos a informação em nossos dispositivos móveis.
O Android Cloud to Device Messaging está disponível para todos os desenvolvedores, mas para utilizá-lo é preciso ir até Sign Up for Android Cloud to Device Messaging e solicitar acesso.
Bom, o processo para utilizar IPN com C2DM é bem simples:
- O usuário acessa a aplicação em seu dispositivo;
- A aplicação faz a chamada ao método de registro do C2DM;
- Após o registro do usuário, o Google fornece um ID único para ele, que a aplicação deverá enviar para a aplicação no lado do servidor;
- A aplicação do lado do servidor recebe o identificador e armazena. Esse ID será utilizado nas comunicações entre servidor e dispositivo;
- Quando o PayPal enviar uma mensagem IPN, utilizamos o ID de registro do usuário e fazemos um POST para o serviço C2DM com o ID da transação;
- O usuário recebe a notificação em seu dispositivo Android;
- A aplicação Android exibe as informações do pagamento.
Como podemos ver, precisaremos de uma aplicação no servidor para receber o identificador único do usuário, as notificações IPN do PayPal e despachá-las para o serviço C2DM.
Google ClientLogin
A aplicação que fará o POST ao serviço C2DM precisa estar autorizada e, para isso, utilizaremos ClientLogin. Como a implementação é feita em três linguagens diferentes, escolhi um padrão de nomenclatura de classes e pacotes para facilitar e simplificar as coisas.
A implementação do ClientLogin é bem simples e o código abaixo dará conta do recado:
Em PHP
com/paypal/ipn/google/auth/ClientLogin.php
<?php
namespace com\paypal\ipn\google\auth;
/**
* Faz a requisição POST ao ClientLogin do Google para obter a autorização
* de acesso para contas Google.
* @author João Batista Neto
*/
class ClientLogin {
/**
* URL do serviço de autorização ClientLogin do Google.
* @var string
*/
const URL = 'https://www.google.com/accounts/ClientLogin';
/**
* Token de autorização.
* @var string
*/
private $auth;
/**
* Obtém o token de autorização do Google utilizando ClientLogin
*
* @param string $accountType Tipo da conta que está solicitando a
* autorização, os valores possíveis são:
* <ul>
* <li>GOOGLE</li>
* <li>HOSTED</li>
* <li>HOSTED_OR_GOOGLE</li>
* </ul>
* @param string $Email Email completo do usuário, incluindo o domínio.
* @param string $Passwd Senha do usuário.
* @param string $source Uma string identificando a aplicação.
* @param string $service Nome do serviço que será solicitada a
* autorização.
* @return string O Token de autorização.
*/
public function getAuth(
$accountType,
$Email,
$Passwd,
$source,
$service ) {
if ( $this->auth === null ) {
$curl = curl_init();
curl_setopt( $curl , CURLOPT_URL , ClientLogin::URL );
curl_setopt( $curl , CURLOPT_RETURNTRANSFER , 1 );
curl_setopt( $curl , CURLOPT_POST , 1 );
curl_setopt( $curl , CURLOPT_POSTFIELDS , http_build_query(
array(
'accountType' => $accountType,
'Email' => $Email,
'Passwd' => $Passwd,
'source' => $source,
'service' => $service
)
) );
$responseStr = curl_exec( $curl );
$responseArr = array();
$matches = array();
curl_close( $curl );
if ( preg_match_all(
"/\n?(?<field>\\w+)\\=(?<value>[^\n]+)/",
$responseStr,
$matches ) ) {
foreach ( $matches[ 'field' ] as $offset => $field ) {
$responseArr[ $field ] = $matches[ 'value' ][ $offset ];
}
if ( isset( $responseArr[ 'Auth' ] ) ) {
$this->auth = $responseArr[ 'Auth' ];
}
}
}
return $this->auth;
}
}
Em Java
com/paypal/ipn/google/auth/ClientLogin.java
package com.paypal.ipn.google.auth;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Faz a requisição POST ao ClientLogin do Google para obter a autorização de
* acesso para contas Google.
*
* @author João Batista Neto
*/
public class ClientLogin {
/**
* URL do serviço de autorização ClientLogin do Google.
*/
public static final String URL = "https://www.google.com/accounts/ClientLogin";
/**
* Token de autorização.
*/
private String auth;
/**
* Obtém o token de autorização do Google utilizando ClientLogin
*
* @param accountType
* {@link String} Tipo da conta que está solicitando a
* autorização, os valores possíveis são:
* <ul>
* <li>GOOGLE</li>
* <li>HOSTED</li>
* <li>HOSTED_OR_GOOGLE</li>
* </ul>
* @param Email
* {@link String} Email completo do usuário, incluindo o domínio.
* @param Passwd
* {@link String} Senha do usuário.
* @param source
* {@link String} Uma string identificando a aplicação.
* @param service
* {@link String} Nome do serviço que será solicitada a
* autorização.
* @return {@link String} O Token de autorização.
*/
public String getAuth(String accountType, String Email, String Passwd,
String source, String service) {
if (auth == null) {
try {
URL url = new URL(URL);
URLConnection conn = url.openConnection();
StringBuilder sb = new StringBuilder();
sb.append("accountType=" + accountType);
sb.append("&Email=" + Email);
sb.append("&Passwd=" + Passwd);
sb.append("&source=" + source);
sb.append("&service=" + service);
conn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(
conn.getOutputStream());
writer.write(sb.toString());
writer.flush();
writer.close();
InputStreamReader in = new InputStreamReader(
conn.getInputStream());
BufferedReader reader = new BufferedReader(in);
sb = new StringBuilder();
String data = null;
while ((data = reader.readLine()) != null) {
String nv[] = data.split("=");
if (nv.length == 2 && nv[0].equals("Auth")) {
auth = nv[0];
break;
}
}
reader.close();
} catch (IOException e) {
Logger.getLogger(ClientLogin.class.getName()).log(Level.SEVERE,
null, e);
}
}
return auth;
}
}
Em C#
com/paypal/ipn/google/auth/ClientLogin.cs
namespace com.paypal.ipn.google.auth {
using System;
using System.IO;
using System.Text;
using System.Net;
/// <summary>
/// Faz a requisição POST ao ClientLogin do Google para obter a autorização de
/// acesso para contas Google.
/// </summary>
public class ClientLogin {
/// <summary>
/// URL do serviço de autorização ClientLogin do Google .
/// </summary>
const string URL = "https://www.google.com/accounts/ClientLogin";
/// <summary>
/// Token de autorização.
/// </summary>
private string Auth;
/// <summary>
/// Obtém o token de autorização do Google utilizando ClientLogin.
/// </summary>
/// <param name="accountType">
/// <see cref="System.String"/> Tipo da conta que está solicitando a
/// autorização, os valores possíveis são:
/// <ul>
/// <li>GOOGLE</li>
/// <li>HOSTED</li>
/// <li>HOSTED_OR_GOOGLE</li>
/// </ul>
/// </param>
/// <param name="Email">
/// <see cref="System.String"/> Email completo do usuário, incluindo o domínio.
/// </param>
/// <param name="Passwd">
/// <see cref="System.String"/> Senha do usuário.
/// </param>
/// <param name="source">
/// Uma <see cref="System.String"/> identificando a aplicação.
/// </param>
/// <param name="service">
/// <see cref="System.String"/> Nome do serviço que será solicitada a autorização.
/// </param>
/// <returns>
/// <see cref="System.String"/> O Token de autorização.
/// </returns>
public string getAuth(string accountType,
string Email,
string Passwd,
string source,
string service) {
if ( Auth == null ) {
HttpWebRequest request = (HttpWebRequest) WebRequest.Create( URL );
StringBuilder sb = new StringBuilder();
sb.Append( "accountType=" + accountType );
sb.Append( "&Email=" + Email );
sb.Append( "&Passwd=" + Passwd );
sb.Append( "&source=" + source );
sb.Append( "&service=" + service );
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
using ( Stream stream = request.GetRequestStream() ) {
UTF8Encoding encoding = new UTF8Encoding();
byte[] bytes = encoding.GetBytes( sb.ToString() );
stream.Write( bytes , 0 , bytes.Length );
}
HttpWebResponse response = (HttpWebResponse) request.GetResponse();
using ( Stream stream = response.GetResponseStream() ) {
string data;
using ( StreamReader reader = new StreamReader( stream , Encoding.UTF8 ) ) {
while ( (data = reader.ReadLine()) != null ) {
string[] nv = data.Split( '=' );
if ( nv.Length == 2 && nv[0].Equals( "Auth" ) ) {
Auth = nv[1];
break;
}
}
reader.Close();
}
}
}
return Auth;
}
}
}
A requisição ao ClientLogin para obter o token não precisa ser feita todas as vezes. A recomendação é que o token seja armazenado pela aplicação server side, mas é importante ter uma política de atualização constante. Como disse anteriormente, o cliente abrirá a aplicação no dispositivo Android, fará o registro no C2DM e receberá um ID único do Google. Esse ID deverá ser enviado para a aplicação server side, que o utilizará para enviar mensagens para o dispositivo.
Android Cloud to Device Messaging
Para a aplicação no dispositivo Android receber uma mensagem do servidor, primeiro precisamos que aplicação envie essa mensagem para o serviço C2DM. Para isso, é preciso que:
- A aplicação envie a mensagem para o serviço C2DM contendo o ID de registro, mensagem e Token de autorização;
- Google receba a mensagem e coloque em uma fila. Se o dispositivo estiver offline, o Google armazena a mensagem;
- Quando o dispositivo esteja online, o Google envie a mensagem para o dispositivo;
- O sistema transmita a mensagem via Intent Broadcast para a aplicação. A aplicação não precisa estar em execução para receber a mensagem.
- A aplicação receba a mensagem e a processe adequadamente.
Vamos precisar de dois participantes, o primeiro é o responsável por enviar as mensagens para o serviço C2DM:
Em PHP
com/paypal/ipn/google/ac2dm/AndroidCloud2DeviceMessaging.php
<?php
namespace com\paypal\ipn\google\ac2dm;
/**
* A classe AndroidCloud2DeviceMessaging faz integração com o serviço
* Android Cloud to Device Messaging (C2DM) para enviar dados para dispositivos
* Android.
*
* @author João Batista Neto
*/
class AndroidCloud2DeviceMessaging {
/**
* URL do serviço C2DM.
* @var string
*/
const URL = 'https://android.apis.google.com/c2dm/send';
/**
* Conjunto de pares key=value que serão enviados para o dispositivo.
* @var array
*/
private $data = array();
/**
* Adiciona um par key=value que será enviado para o dispositivo android.
*
* @param string $key A chave que será enviada para o dispositivo.
* @param string $value O valor da chave.
* @param string $collapseKey Chave de agrupamento que será utilizado pelo
* Google para evitar que várias mensagens do mesmo tipo sejam
* enviadas para o usuário de uma vez quando o dispositivo fique
* online.
*/
public function addData( $key , $value , $collapseKey ) {
if ( !isset( $this->data[ $collapseKey ] ) ) {
$this->data[ $collapseKey ] = array();
}
$this->data[ $collapseKey ][ 'data.' . $key ] = $value;
}
/**
* Remove todas os pares key=value.
*/
public function clear() {
$this->data = array();
}
/**
* Envia a mensagem para o servidor C2DM.
*
* @param string $registrationId ID de registro do dispositivo android.
* @param string $auth Token de autorização
* @see com\google\auth\ClientLogin::getAuth()
*/
public function send( $registrationId , $auth ) {
$curl = curl_init();
curl_setopt( $curl , CURLOPT_URL , AndroidCloud2DeviceMessaging::URL );
curl_setopt( $curl , CURLOPT_SSL_VERIFYPEER , false );
curl_setopt( $curl , CURLOPT_RETURNTRANSFER , 1 );
curl_setopt( $curl , CURLOPT_POST , 1 );
curl_setopt( $curl , CURLOPT_HTTPHEADER , array(
'Authorization: GoogleLogin auth=' . $auth
) );
foreach ( $this->data as $collapseKey => $data ) {
$data[ 'registration_id' ] = $registrationId;
$data[ 'collapse_key' ] = $collapseKey;
curl_setopt( $curl , CURLOPT_POSTFIELDS , http_build_query($data));
var_dump( curl_exec( $curl ) );
}
curl_close( $curl );
}
}
Em Java
com/paypal/ipn/google/ac2dm/AndroidCloud2DeviceMessaging.java
package com.paypal.ipn.google.ac2dm;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
/**
* A classe AndroidCloud2DeviceMessaging faz integração com o serviço Android
* Cloud to Device Messaging (C2DM) para enviar dados para dispositivos Android.
*
* @author João Batista Neto
*/
public class AndroidCloud2DeviceMessaging {
/**
* O certificado do Google não cobre android.apis.google.com, então será
* preciso verificar e fazer a validação do certificado manualmente.
*/
private static final String HOSTNAME = "android.apis.google.com";
/**
* URL do serviço C2DM.
*/
public static final String URL = "https://android.apis.google.com/c2dm/send";
/**
* Conjunto de pares key=value que serão enviados para o dispositivo.
*/
private Map<String, Map<String, String>> data;
public AndroidCloud2DeviceMessaging() {
data = new HashMap<String, Map<String, String>>();
}
/**
* Adiciona um par key=value que será enviado para o dispositivo android.
*
* @param key {@link String}
* A chave que será enviada para o dispositivo.
* @param value {@link String}
* O valor da chave.
* @param collapseKey {@link String}
* Chave de agrupamento que será utilizado pelo
* Google para evitar que várias mensagens do mesmo tipo sejam
* enviadas para o usuário de uma vez quando o dispositivo fique
* online.
*/
public void addData(String key, String value, String collapseKey) {
Map<String, String> map;
if ((map = data.get(collapseKey)) == null) {
map = new HashMap<String, String>();
data.put(collapseKey, map);
}
map.put(key, value);
}
/**
* Remove todas os pares key=value.
*/
public void clear() {
data.clear();
}
/**
* Envia a mensagem para o servidor C2DM.
*
* @param registrationId {@link String} ID de registro do dispositivo
* android.
* @param auth {@link String} Token de autorização.
* @see {@link com.paypal.ipn.google.auth.ClientLogin#getAuth}
*/
public void send(String registrationId, String auth) {
try {
URL url = new URL(URL);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return hostname.equals(HOSTNAME);
}
});
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
conn.setRequestProperty("Authorization", "GoogleLogin auth=" + auth);
for (String collapseKey : data.keySet()) {
StringBuilder sb = new StringBuilder();
Map<String, String> nv = data.get(collapseKey);
sb.append("registration_id=" + registrationId);
sb.append("&collapse_key=" + collapseKey);
for (Entry<String, String> entry : nv.entrySet()) {
sb.append("&data." + entry.getKey());
sb.append("=");
sb.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
OutputStreamWriter writer = new OutputStreamWriter(
conn.getOutputStream());
writer.write(sb.toString());
writer.flush();
writer.close();
if (conn.getResponseCode() != 200) {
Logger.getLogger(
AndroidCloud2DeviceMessaging.class.getName()).log(
Level.SEVERE, conn.getResponseMessage());
}
}
} catch (IOException e) {
Logger.getLogger(AndroidCloud2DeviceMessaging.class.getName()).log(
Level.SEVERE, null, e);
}
}
}
Em C#
com/google/c2dm/AndroidCloud2DeviceMessaging.cs
namespace com.paypal.ipn.google.ac2dm {
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Web;
/// <summary>
/// A classe AndroidCloud2DeviceMessaging faz integração com o serviço Android
/// Cloud to Device Messaging (C2DM) para enviar dados para dispositivos Android.
///
/// Author: João Batista Neto
/// </summary>
public class AndroidCloud2DeviceMessaging {
/// <summary>
/// URL do serviço C2DM.
/// </summary>
public const String URL = "https://android.apis.google.com/c2dm/send";
/// <summary>
/// Conjunto de pares key=value que serão enviados para o dispositivo.
/// </summary>
private Dictionary<string,NameValueCollection> data;
public AndroidCloud2DeviceMessaging () {
data = new Dictionary<string, NameValueCollection>();
}
/// <summary>
/// Adiciona um par key=value que será enviado para o dispositivo android.
/// </summary>
/// <param name="key">
/// <see cref="System.String"/> A chave que será enviada para o dispositivo.
/// </param>
/// <param name="value">
/// <see cref="System.String"/> O valor da chave.
/// </param>
/// <param name="collapseKey">
/// <see cref="System.String"/> Chave de agrupamento que será utilizado pelo
/// Google para evitar que várias mensagens do mesmo tipo sejam
/// enviadas para o usuário de uma vez quando o dispositivo fique
/// online.
/// </param>
public void addData(string key,string value,string collapseKey) {
NameValueCollection nv;
if ( !data.ContainsKey(collapseKey) ) {
nv = new NameValueCollection();
data.Add( collapseKey , nv );
} else {
nv = data[collapseKey];
}
nv.Set(key,value);
}
/// <summary>
/// Remove todas os pares key=value.
/// </summary>
public void clear() {
data.Clear();
}
/// <summary>
/// Envia a mensagem para o servidor C2DM.
/// </summary>
/// <param name="registrationId">
/// <see cref="System.String"/> ID de registro do dispositivo android.
/// </param>
/// <param name="auth">
/// <see cref="System.String"/> Token de autorização.
/// </param>
/// <seealso cref="com.paypal.ipn.google.auth.ClientLogin#getAuth"/>
public void send( string registrationId,string auth ) {
ServicePointManager.ServerCertificateValidationCallback = Validator;
HttpWebRequest request = (HttpWebRequest) WebRequest.Create( URL );
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.Headers.Add( "Authorization" , "GoogleLogin auth=" + auth );
foreach ( string collapseKey in data.Keys ) {
StringBuilder sb = new StringBuilder();
NameValueCollection nv = data[collapseKey];
sb.Append("registration_id=" + registrationId);
sb.Append("&collapse_key=" + collapseKey);
foreach ( string key in nv.AllKeys ) {
sb.Append("&data." + key );
sb.Append("=");
sb.Append(HttpUtility.UrlEncode(nv[key]));
}
using ( Stream stream = request.GetRequestStream() ) {
UTF8Encoding encoding = new UTF8Encoding();
byte[] bytes = encoding.GetBytes( sb.ToString() );
stream.Write( bytes , 0 , bytes.Length );
}
}
}
/// <summary>
/// O certificado do Google não cobre android.apis.google.com, então será
/// preciso verificar e fazer a validação do certificado manualmente.
/// </summary>
/// <param name="sender">
/// <see cref="System.Object"/>
/// </param>
/// <param name="certificate">
/// <see cref="X509Certificate"/>
/// </param>
/// <param name="chain">
/// <see cref="X509Chain"/>
/// </param>
/// <param name="sslPolicyErrors">
/// <see cref="SslPolicyErrors"/>
/// </param>
/// <returns>
/// <see cref="System.Boolean"/> Retornamos
/// </returns>
bool Validator(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors ) {
return true;
}
}
}
Com isso, definimos o manipulador de notificação instantânea de pagamento para receber as mensagens do PayPal, verificá-las e só então despachá-las para o dispositivo Android:
Instant payment notification
Como a validação da mensagem é recorrente para qualquer manipulador, podemos ter um participante que faça essa validação e deixe a parte de regras específicas de negócio para um manipulador abstrato:
Em PHP
com/paypal/ipn/InstantPaymentNotification.php
<?php
namespace com\paypal\ipn;
use \BadMethodCallException;
/**
* Observador de Notificações de Pagamento Instantâneo.
*
* @author João Batista Neto
*/
class InstantPaymentNotification {
/**
* Endpoint de produção.
* @var string
*/
const HOST = 'https://www.paypal.com';
/**
* Endpoint de produção.
* @var string
*/
const SANDBOX_HOST = 'https://www.sandbox.paypal.com';
/**
* @var string
*/
private $endpoint = InstantPaymentNotification::HOST;
/**
* @var com\paypal\ipn\IPNHandler
*/
private $ipnHandler;
/**
* Constroi o observador no notificação instantânea de pagamento informando
* o ambiente que será utilizado para validação.
*
* @param boolean $sandbox Define se será utilizado o Sandbox
* @throws InvalidArgumentException
*/
public function __construct( $sandbox = false ) {
if ( !!$sandbox ) {
$this->endpoint = InstantPaymentNotification::SANDBOX_HOST;
}
$this->endpoint .= '/cgi-bin/webscr?cmd=_notify-validate';
}
/**
* Aguarda por notificações de pagamento instantânea; Caso uma nova
* notificação seja recebida, faz a verificação e notifica um manipulador
* com o status (verificada ou não) e a mensagem recebida.
*
* @param array $post Dados postatos pelo PayPal.
* @see InstantPaymentNotification::setIPNHandler()
* @throws BadMethodCallException Caso o método seja chamado antes de um
* manipulador ter sido definido ou nenhum email de recebedor
* tenha sido informado.
*/
public function listen( array $post ) {
if ( $this->ipnHandler !== null && isset( $post[ 'receiver_email' ] ) ) {
$curl = curl_init ();
curl_setopt( $curl, CURLOPT_URL, $this->endpoint );
curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl, CURLOPT_POST, 1 );
curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query(
$post
) );
$response = curl_exec( $curl );
$error = curl_error( $curl );
$errno = curl_errno( $curl );
curl_close ( $curl );
if ( empty( $error ) && $errno == 0 ) {
$this->ipnHandler->handle(
$response == 'VERIFIED', $post
);
}
}
}
/**
* Define o objeto que irá manipular as notificações de pagamento
* instantâneas enviadas pelo PayPal.
*
* @param com\paypal\ipn\IPNHandler $ipnHandler
*/
public function setIPNHandler( IPNHandler $ipnHandler ) {
$this->ipnHandler = $ipnHandler;
}
}
com/paypal/ipn/IPNHandler.php
<?php
namespace com\paypal\ipn;
/**
* Interface para definição de um manipulador de notificação
* de pagamento instantânea.
*
* @author João Batista Neto
*/
interface IPNHandler {
/**
* Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
*
* @param boolean $isVerified Identifica que a mensagem foi verificada
* como tendo sido enviada pelo PayPal.
* @param array $message Mensagem completa enviada pelo PayPal.
*/
public function handle( $isVerified, array $message );
}
E a implementação que fará o trabalho:
com/paypal/ipn/sample/IPNToAC2DMHandler.php
<?php
namespace com\paypal\ipn\sample;
use com\paypal\ipn\google\ac2dm\AndroidCloud2DeviceMessaging;
use com\paypal\ipn\IPNHandler;
/**
* Manipulador de notificação instantânea de pagamento que envia a
* mensagem para dispositivos Android.
*
* @author João Batista Neto
*/
class IPNToAC2DMHandler implements IPNHandler {
/**
* @var com\paypal\ipn\sample\SampleModel
*/
private $model;
public function __construct( SampleModel $model ) {
$this->model = $model;
}
/* (non-PHPdoc)
* @see com\paypal\ipn\IPNHandler#handle()
*/
public function handle( $isVerified, array $message ) {
if ( $isVerified && isset( $message[ 'receiver_email' ] ) ) {
$ac2dm = new AndroidCloud2DeviceMessaging();
foreach ( $message as $field => $value ) {
$ac2dm->addData( $field , $value, 'ipn' );
}
$ac2dm->send(
$this->model->getRegistrationId( $message['receiver_email' ] ),
$this->model->getAuth() );
}
}
}
Em Java
com/paypal/ipn/InstantPaymentNotification.java
package com.paypal.ipn;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
/**
* Observador de Notificações de Pagamento Instantâneo.
*
* @author João Batista Neto
*/
public class InstantPaymentNotification {
/**
* HOST de produção.
*/
private static final String HOST = "https://www.paypal.com";
/**
* HOST do Sandbox
*/
private static final String SANDBOX_HOST = "https://www.sandbox.paypal.com";
/**
* Endpoint que será utilizado na validação
*/
private String endpoint = InstantPaymentNotification.HOST;
private IPNHandler ipnHandler;
public InstantPaymentNotification() {
this(false);
}
/**
* Constroi o observador no notificação instantânea de pagamento informando
* o ambiente que será utilizado para validação.
*
* @param sandbox
* {@link Boolean} Define se será utilizado o ambiente de
* produção ou o Sandbox.
*/
public InstantPaymentNotification(Boolean sandbox) {
if (sandbox) {
endpoint = InstantPaymentNotification.SANDBOX_HOST;
}
endpoint += "/cgi-bin/webscr?cmd=_notify-validate";
}
/**
* Aguarda por notificações de pagamento instantânea; Caso uma nova
* notificação seja recebida, faz a verificação e notifica um manipulador
* com o status (verificada ou não) e a mensagem recebida.
*
* @param post
* {@link HttpServletRequest} Dados postatos pelo PayPal.
* @see {@link InstantPaymentNotification#setIPNHandler()}
*/
public void listen(HttpServletRequest post) {
if (ipnHandler != null && post.getParameter("receiver_email") != null) {
try {
BufferedReader reader = post.getReader();
StringBuffer sb = new StringBuffer();
String data;
while ((data = reader.readLine()) != null) {
sb.append(data);
}
URL url = new URL(endpoint);
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(
conn.getOutputStream());
writer.write(sb.toString());
writer.flush();
writer.close();
InputStreamReader in = new InputStreamReader(
conn.getInputStream());
reader = new BufferedReader(in);
sb = new StringBuffer();
data = null;
while ((data = reader.readLine()) != null) {
sb.append(data);
}
ipnHandler.handle(sb.toString().equals("VERIFIED"), post);
} catch (IOException e) {
Logger.getLogger(InstantPaymentNotification.class.getName())
.log(Level.SEVERE, null, e);
}
}
}
/**
* Define o objeto que irá manipular as notificações de pagamento
* instantâneas enviadas pelo PayPal.
*
* @param ipnHandler
* {@link IPNHandler}
*/
public void setIPNHandler(IPNHandler ipnHandler) {
this.ipnHandler = ipnHandler;
}
}
com/paypal/ipn/IPNHandler.java
package com.paypal.ipn;
import javax.servlet.http.HttpServletRequest;
/**
* Interface para definição de um manipulador de notificação de pagamento
* instantânea.
*
* @author João Batista Neto
*/
public interface IPNHandler {
/**
* Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
*
* @param isVerified
* {@link Boolean} Identifica que a mensagem foi verificada como
* tendo sido enviada pelo PayPal.
* @param message
* {@link HttpServletRequest} Mensagem completa enviada pelo
* PayPal.
*/
public void handle(Boolean isVerified, HttpServletRequest message);
}
com/paypal/ipn/sample/IPNToAC2DMHandler.java
package com.paypal.ipn.sample;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import com.paypal.ipn.IPNHandler;
import com.paypal.ipn.google.ac2dm.AndroidCloud2DeviceMessaging;
/**
* Manipulador de notificação instantânea de pagamento que envia a mensagem para
* dispositivos Android.
*
* @author João Batista Neto
*/
public class IPNToAC2DMHandler implements IPNHandler {
private SampleModel model;
public IPNToAC2DMHandler(SampleModel model) {
this.model = model;
}
/*
* (non-Javadoc)
*
* @see com.paypal.ipn.IPNHandler#handle()
*/
@SuppressWarnings("rawtypes")
@Override
public void handle(Boolean isVerified, HttpServletRequest message) {
if (isVerified) {
AndroidCloud2DeviceMessaging ac2dm = new AndroidCloud2DeviceMessaging();
Enumeration fields = message.getParameterNames();
while (fields.hasMoreElements()) {
String field = (String) fields.nextElement();
ac2dm.addData(field, message.getParameter(field), "ipn");
}
ac2dm.send(model.getRegistrationId(message
.getParameter("receiver_email")), model.getAuth());
}
}
}
Em C#
com/paypal/ipn/InstantPaymentNotification.cs
namespace com.paypal.ipn {
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Mvc;
/// <summary>
/// Observador de Notificações de Pagamento Instantâneo.
///
/// Author: João Batista Neto
/// </summary>
public class InstantPaymentNotification {
/// <summary>
/// Endpoint de produção.
/// </summary>
public const string HOST = "https://www.paypal.com";
/// <summary>
/// Endpoint do Sandbox
/// </summary>
public const string SANDBOX_HOST = "https://www.sandbox.paypal.com";
/// <summary>
/// Endpoint que será utilizado na verificação
/// </summary>
private string endpoint = InstantPaymentNotification.HOST;
/// <summary>
/// Manipulador de notificação instantânea de pagamento.
/// </summary>
private IPNHandler handler;
public InstantPaymentNotification() :this(false) {}
/// <summary>
/// Constroi o observador no notificação instantânea de pagamento informando
/// o ambiente que será utilizado para validação.
/// </summary>
/// <param name="sandbox">
/// <see cref="System.Boolean"/> Define se será utilizado o ambiente de
/// produção ou o Sandbox.
/// </param>
public InstantPaymentNotification ( bool sandbox ) {
if ( sandbox ) {
endpoint = InstantPaymentNotification.SANDBOX_HOST;
}
endpoint += "/cgi-bin/webscr?cmd=_notify-validate";
}
/// <summary>
/// Aguarda por notificações de pagamento instantânea; Caso uma nova
/// notificação seja recebida, faz a verificação e notifica um manipulador
/// com o status (verificada ou não) e a mensagem recebida.
/// </summary>
/// <param name="post">
/// A <see cref="FormCollection"/> com os dados postados pelo PayPal
/// </param>
/// <seealso cref="InstantPaymentNotification#setIPNHandler()"/>
public void listem( FormCollection post ) {
if ( handler != null && post[ "receiver_email" ] != null ) {
HttpWebRequest request = (HttpWebRequest) WebRequest.Create( endpoint );
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
StringBuilder sb = new StringBuilder();
foreach ( string field in post ) {
if ( sb.Length != 0 ) {
sb.Append( "&" );
}
sb.Append( field );
sb.Append( "=" );
sb.Append( post[ field ] );
}
using ( Stream stream = request.GetRequestStream() ) {
UTF8Encoding encoding = new UTF8Encoding();
byte[] bytes = encoding.GetBytes( sb.ToString() );
stream.Write( bytes , 0 , bytes.Length );
}
HttpWebResponse response = (HttpWebResponse) request.GetResponse();
using ( Stream stream = response.GetResponseStream() ) {
using ( StreamReader reader = new StreamReader( stream , Encoding.UTF8 ) ) {
string data = reader.ReadToEnd();
reader.Close();
handler.handle( data.Equals( "VERIFIED" ) , post );
}
}
}
}
/// <summary>
/// Define o objeto que irá manipular as notificações de pagamento
/// instantâneas enviadas pelo PayPal.
/// </summary>
/// <param name="handler">
/// <see cref="IPNHandler"/>
/// </param>
public void setIPNHandler( IPNHandler handler ) {
this.handler = handler;
}
}
}
com/paypal/ipn/IPNHandler.cs
namespace com.paypal.ipn {
using System;
using System.Web.Mvc;
/// <summary>
/// Interface para definição de um manipulador de notificação
/// de pagamento instantânea.
///
/// Author: João Batista Neto
/// </summary>
public interface IPNHandler {
/// <summary>
/// Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
/// </summary>
/// <param name="isVerified">
/// <see cref="System.Boolean"/> isVerified Identifica que a mensagem foi
/// verificada como tendo sido enviada pelo PayPal.
/// </param>
/// <param name="form">
/// <see cref="FormCollection"/> Mensagem completa enviada pelo PayPal.
/// </param>
void handle( bool isVerified , FormCollection message );
}
}
com/paypal/ipn/sample/IPNToAC2DMHandler.cs
namespace com.paypal.ipn.sample {
using System;
using System.Web.Mvc;
using com.paypal.ipn;
using com.paypal.ipn.google.ac2dm;
/// <summary>
/// Manipulador de notificação instantânea de pagamento que envia a
/// mensagem para dispositivos Android.
/// </summary>
public class IPNToAC2DMHandler :IPNHandler {
private SampleModel model;
public IPNToAC2DMHandler ( SampleModel model ) {
this.model = model;
}
/// <summary>
/// Envia as notificações instantânea de pagamento para dispositivos
/// Android caso a mensagem tenha sido verificada pelo PayPal.
/// </summary>
/// <param name="isVerified">
/// <see cref="System.Boolean"/> TRUE caso o PayPal tenha verificado
/// a mensagem.
/// </param>
/// <param name="message">
/// <see cref="FormCollection"/> A mensagem enviada pelo PayPal
/// </param>
public void handle( bool isVerified , FormCollection message ) {
if ( isVerified ) {
AndroidCloud2DeviceMessaging ac2dm = new AndroidCloud2DeviceMessaging();
foreach ( string field in message ) {
ac2dm.addData( field , message[field] , "ipn" );
}
ac2dm.send(model.getRegistrationId(message["receiver_email"]),
model.getAuth() );
}
}
}
}
Aplicação Android
No Eclipse nós criamos um novo “Projeto Android”:
O Android Cloud to Device Messaging está disponível desde a versão 2.2, então utilizem a que for mais adequada para o projeto.
Em seguida, algumas configurações sobre o projeto:
E pronto, temos o esqueleto do projeto Android:
Para poder utilizar o serviço C2DM, precisaremos adicionar algumas permissões no AndroidManifest.xml. Ele ficará assim:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.paypal.ipn"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="11" />
<permission
android:name="com.paypal.ipn.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.paypal.ipn.permission.C2D_MESSAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".RegisterActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".C2DMReceiver" />
<!--
Apenas servidores C2DM podem enviar mensagens para a aplicação.
Se a permissão não estiver definida, qualquer outra aplicação pode
gerar a mensagem.
-->
<receiver
android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<!--
Recebe a mensagem
-->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.paypal.ipn" />
</intent-filter>
<!--
Recebe o ID de registro
-->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="com.paypal.ipn" />
</intent-filter>
</receiver>
</application>
</manifest>
Nossa interface de usuário é bastante simples:
layout/register.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageViewBrand"
android:contentDescription="@string/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="30dp"
android:src="@drawable/ic_brand" />
<EditText
android:id="@+id/textEmailAddress"
android:layout_width="676dp"
android:layout_height="wrap_content"
android:hint="@string/register_email"
android:inputType="textEmailAddress" >
<requestFocus />
</EditText>
<Button
android:id="@+id/registerButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="register"
android:text="@string/register_button" />
</LinearLayout>
Assim como o processo de registro:
// Usamos a API Intent para para obter o ID de registro
Intent regIntent = new Intent( "com.google.android.c2dm.intent.REGISTER" );
// Identificamos a aplicação
registrationIntent.putExtra("app",
PendingIntent.getBroadcast(context, 0, new Intent(), 0));
// Identificamos a conta que o servidor usará para enviar as notificações
registrationIntent.putExtra("sender", senderId);
context.startService(registrationIntent);
Como a Google já providenciou um framework para esse trabalho, precisamos apenas utilizar o código disponibilizado:
com/paypal/ipn/RegisterActivity.java
package com.paypal.ipn;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.c2dm.C2DMessaging;
public class RegisterActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.register);
((EditText) findViewById(R.id.textEmailAddress))
.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent e) {
if (e.getAction() == KeyEvent.ACTION_DOWN
&& keyCode == KeyEvent.KEYCODE_ENTER) {
register(v);
}
return false;
}
});
}
public void register(View view) {
Toast.makeText(this, "Starting", Toast.LENGTH_LONG).show();
((EditText) findViewById(R.id.textEmailAddress)).setEnabled(false);
((Button) findViewById(R.id.registerButton)).setEnabled(false);
String email = ((EditText) findViewById(R.id.textEmailAddress))
.getText().toString();
SharedPreferences prefs = getSharedPreferences(getResources()
.getString(R.string.app_id), Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("receiverEmail", email);
editor.commit();
C2DMessaging.register(this, email);
}
}
E o responsável por receber as notificações:
com/paypal/ipn/C2DMReceiver.java
package com.paypal.ipn;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import com.google.android.c2dm.C2DMBaseReceiver;
public class C2DMReceiver extends C2DMBaseReceiver {
public C2DMReceiver() {
super("brasil@x.com");
}
/**
* Recebe o id de registro e envia para a nossa aplicação no servidor, que
* manipulará as notificações instantânea de pagamento.
*/
@Override
public void onRegistered(Context context, String registrationId)
throws java.io.IOException {
SharedPreferences prefs = context.getSharedPreferences(
context.getString(R.string.app_id), Context.MODE_PRIVATE);
URL url = new URL(context.getString(R.string.register_server));
URLConnection conn = url.openConnection();
StringBuilder sb = new StringBuilder();
sb.append("registrationId=" + registrationId);
sb.append("&receiverEmail=" + prefs.getString("receiverEmail", ""));
conn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(
conn.getOutputStream());
writer.write(sb.toString());
writer.flush();
writer.close();
Editor editor = prefs.edit();
editor.putString("registrationId", registrationId);
editor.commit();
};
/**
* Mensagem recebida!
*/
@Override
protected void onMessage(Context context, Intent intent) {
String ns = Context.NOTIFICATION_SERVICE;
// Pegamos o gerenciados de notificação
NotificationManager notificationManager = (NotificationManager) getSystemService(ns);
// Ícone que aparecerá na notificação
int icon = R.drawable.ic_launcher;
// Mensagem que aparecerá quando a notificação aparecer.
CharSequence tickerText = context
.getString(R.string.ipn_notification_ticker);
long when = System.currentTimeMillis();
// Criamos a notificação, definindo o título, texto, som, etc
Notification notification = new Notification(icon, tickerText, when);
CharSequence contentTitle = context
.getString(R.string.ipn_notification_title);
CharSequence contentText = context
.getString(R.string.ipn_notification_text);
Intent notificationIntent = new Intent(this, RegisterActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
notificationIntent, 0);
notification.defaults |= Notification.DEFAULT_SOUND;
notification.setLatestEventInfo(context, contentTitle, contentText,
contentIntent);
// Exibimos a notificação
notificationManager.notify(1, notification);
}
@Override
public void onError(Context context, String errorId) {
// opz
}
}
É isso! Para testar o recebimento das notificações no dispositivo Android basta ir até a página Testando Notificações Instantâneas de Pagamento, no PayPal Sandbox
Abaixo algumas telas capturadas do dispositivo rodando o exemplo:
O código utilizado no exemplo está disponível no perfil do PayPal X Brasil no github: PayPal X Brasil