Seções iMasters
Android + APIs + APIs PayPal + E-commerce + Flex + Mobile + PHP

Aplicação mobile com Express Checkout

Olá, pessoal.

Como temos percebido, com a popularização dos dispositivos moveis, surgem novos desafios e novas oportunidades; da mesma forma, surgem novas dúvidas e discussões.

Uma dessas discussões é sobre ter um site adaptado para dispositivos móveis ou ter uma aplicação para ser instalada nesses dispositivos.

De fato, existem linhas que defendem com veemência o desenvolvimento de uma aplicação móvel, outros, com convicção, garantem que é melhor ter o site, que deve ser adaptado.

Não vamos entrar no mérito dessa discussão, vamos apenas ilustrar as possibilidades que temos, os desafios e, ao final, teremos uma aplicação móvel utilizando a versão mobile do checkout PayPal.

O primeiro passo para começarmos o desenvolvimento de uma experiência de checkout é compreender, do ponto de vista do cliente, como isso funciona.

Qual seria o motivo pelo qual uma pessoa instalaria uma aplicação de uma loja em seu celular ou tablet?

Quem respondeu “Para comprar produtos da loja” está totalmente correto.

O único motivo que leva alguém a instalar uma aplicação de venda de produtos em um dispositivo móvel é para comprar facilmente um produto dessa loja.

Então já temos um ponto de partida:

ux navigation

Se o usuário quer navegar entre os produtos, precisamos oferecer a ele uma forma fácil de fazer isso, e precisamos, de fato, evitar qualquer coisa que impeça isso ou que ele precise ficar clicando em diversos lugares para chegar até a navegação de produtos.

Então, a primeira coisa que o cliente precisa ver em uma aplicação móvel são os produtos; e depois?

Muitas vezes, o cliente já conhece a loja e sabe exatamente o que ele quer; existem casos de o cliente não querer, sequer, passar por um carrinho de compras, ele abriu a aplicação, viu o produto e quer comprá-lo.

buy now

A loja não pode dificultar o desejo de “Comprar Agora” do cliente, não pode tentar forçá-lo a comprar mais, porque isso poderá fazê-lo não comprar de forma alguma.

Bom, temos agora um cliente navegando entre produtos e podendo comprar qualquer produto em qualquer momento. Mas e se o cliente quiser comprar mais de um item da loja? Obrigar o cliente a ir a um carrinho é um erro, mas outro erro maior ainda é não oferecer a possibilidade de o cliente comprar mais de um item. Temos que ter um carrinho também:

add to cart

Ok, temos um carrinho. E agora ?

Não basta ter o carrinho, o cliente precisa conseguir visualizá-lo, revisar os itens e fazer o checkout:

ux cart navigation

Bom, como devem ter percebido, o “Comprar Agora” é um checkout rápido, mas oferecemos também a opção de fazer um checkout explicitamente clicando em um botão específico.

O fluxo de um checkout convencional seria parecido com a sequência abaixo:

  1. Cliente decide fazer checkout.
  2. Informa os dados de entrega.
  3. Escolhe uma forma de pagamento.
  4. Faz uma revisão do pedido e efetua o pagamento.
  5. Recebe uma confirmação do pedido

Com Express Checkout, o fluxo é um tanto parecido:

  1. Cliente decide fazer checkout.
  2. Se não estiver logado, faz login no PayPal.
  3. Faz uma revisão do pedido no PayPal.
  4. Confirma o pagamento.
  5. Recebe uma confirmação do pedido.

Agora que já compreendemos a experiência do usuário e o fluxo desde a intenção de compra até a concretização de uma venda, vamos ver como implementar isso.

No último artigo sobre Express Checkout, mostramos como fazer uma Integração com Express Checkout em um site convencional.

Utilizaremos exatamente a mesma estrutura do último artigo para fazer a integração em nossa aplicação mobile, ou seja, a aplicação que está no servidor que fará a comunicação com o PayPal. E o motivo é muito simples: quando um usuário está utilizando um dispositivo móvel, não temos como saber de onde ele o está usando. Ele pode estar, por exemplo, utilizando uma rede wireless em um shopping.

Se a rede desse shopping não for criptografada, um usuário mal intencionado pode lançar um ataque chamado “Man in the Middle” e interceptar a comunicação.

Se estivermos fazendo a comunicação diretamente entre o dispositivo móvel, e os dados forem interceptados, teremos um comprometimento total da segurança:

  1. Do cliente.
  2. Da sua loja.
  3. Do PayPal.

Isso porque, ao fazer a integração direta do dispositivo móvel, poderemos estar trafegando informações altamente sigilosas, como usuário, senha e assinatura da API PayPal em uma rede não criptografada. Isso causaria uma exposição totalmente inaceitável.

Como regra geral, jamais trafegue informações sigilosas de um dispositivo móvel, ao contrário, prefira enviar apenas informações não sigilosas para uma aplicação de back-end e, nessa aplicação, faça o tráfego das outras informações.

Bom, nesse caso, já temos então tudo o que precisamos para fazer a integração com o PayPal; vamos apenas dar uma melhorada naquele código:

checkout.php

 <?php
require 'util.php';

$total = filter_input( INPUT_POST , 'total' , FILTER_VALIDATE_FLOAT );
$user = 'usuario.do.vendedor';
$pswd = 'senha';
$signature = 'ASSINATURA';

if ( is_float( $total ) ) {
    $baseURL = 'http://dominio.com/';
    $responseNvp = call( $user , $pswd , $signature,
        'SetExpressCheckout' , array(
            'PAYMENTREQUEST_0_AMT'              => sprintf( '%.02f' , $total ),
            'PAYMENTREQUEST_0_PAYMENTACTION'    => 'Sale',
            'PAYMENTREQUEST_0_CURRENCYCODE'     => 'BRL',
            'LOCALECODE'                        => 'pt_BR',
            'RETURNURL'                         => $baseURL . '/retorno.php',
            'CANCELURL'                         => $baseURL . '/cancelamento.php',
        ) , true
    );

    if ( isset( $responseNvp[ 'ACK' ] ) && $responseNvp[ 'ACK' ] == 'Success' ) {
        showOutput( $responseNvp[ 'TOKEN' ] );
    } else {
        showOutput( 'OPZ' );
    }
} else {
    showOutput( 'OPZ' );
}

Como podem ver, diferentemente do último código, temos um arquivo novo, chamado util.php:

util.php

 <?php
/**
* Exibe uma saída XML
* @param   string $message Uma mensagem qualquer
*/
function showOutput( $message ) {
    header( 'Content-Type: text/xml; charset=UTF-8' );
    echo '<?xml version="1.0"?>' , PHP_EOL;
    echo '<response>';
    echo '<message>' , $message , '</message>';
    echo '</response>';
}

/**
* Efetua uma chamada a uma operação do PayPal
* @param   string $user Nome do usuário
* @param   string $pwd Senha do usuário
* @param   string $signature Assinatura de acesso
* @param   string $operation Operação que será executada
* @param   array $nvp Campos que serão enviados com a requisição
* @param   boolean $close Indica se a conexão deverá ser fechada
* @return  array Matriz associativa com os pares Nome=Valor retornados
*/
function call( $user , $pwd , $signature , $operation , array $nvp , $close = false ) {
    static $curl = null;

    $matches = array();
    $response = array();

    $nvp[ 'VERSION'     ] = '64';
    $nvp[ 'METHOD'      ] = $operation;
    $nvp[ 'PWD'         ] = $pwd;
    $nvp[ 'USER'        ] = $user;
    $nvp[ 'SIGNATURE'   ] = $signature;

    if ( !is_resource( $curl ) ) {
        $curl = curl_init();
    }

    curl_setopt( $curl , CURLOPT_URL , 'https://api-3t.sandbox.paypal.com/nvp' );
    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( $nvp ) );

    if ( preg_match_all( '/(?<name>[^\=]+)\=(?<value>[^&]+)&?/' , urldecode( curl_exec( $curl ) ) , $matches ) ) {
        foreach ( $matches[ 'name' ] as $offset => $name ) {
            $response[ $name ] = $matches[ 'value' ][ $offset ];
        }
    }

    if ( $close ) {
        curl_close( $curl );
    }

    return $response;
}

Com isso, nosso código do retorno também sofreu algumas modificações:

retorno.php

 <?php
require 'util.php';

if ( isset( $_GET[ 'token' ] ) ) {
    $token = $_GET[ 'token' ];
    $user = 'usuario.do.vendedor';
    $pswd = 'senha';
    $signature = 'ASSINATURA';

    $responseNvp = call( $user , $pswd , $signature,
        'GetExpressCheckoutDetails' , array(
            'TOKEN' => $token,
        )
    );

    if ( isset( $responseNvp[ 'TOKEN' ] ) && isset( $responseNvp[ 'ACK' ] ) ) {
        if ( $responseNvp[ 'TOKEN' ] == $token && $responseNvp[ 'ACK' ] == 'Success' ) {
            $responseNvp = call( $user , $pswd , $signature,
                'DoExpressCheckoutPayment' , array(
                    'TOKEN'                             => $token,
                    'PAYERID'                           => $responseNvp[ 'PAYERID' ],
                    'PAYMENTREQUEST_0_AMT'              => $responseNvp[ 'PAYMENTREQUEST_0_AMT' ],
                    'PAYMENTREQUEST_0_CURRENCYCODE'     => $responseNvp[ 'PAYMENTREQUEST_0_CURRENCYCODE' ],
                    'PAYMENTREQUEST_0_PAYMENTACTION'    => 'Sale',
                ) , true
            );

            if ( isset( $responseNvp[ 'PAYMENTINFO_0_PAYMENTSTATUS' ] ) && $responseNvp[ 'PAYMENTINFO_0_PAYMENTSTATUS' ] == 'Completed' ) {
                showOutput( 'SUCESSO' );
            } else {
                showOutput( 'OPZ' );
            }
        } else {
            showOutput( 'OPZ' );
        }
    } else {
        showOutput( 'OPZ' );
    }
} else {
    showOutput( 'OPZ' );
}

Para construir a aplicação mobile, vamos utilizar o Adobe AIR como plataforma. Abaixo, a lista de tudo o que precisaremos:

  1. Flex SDK – Open Source
  2. Adobe AIR SDK
  3. Android SDK

Vamos utilizar o Adobe AIR, pois, desde a versão 2.6, podemos escrever uma única aplicação que rode tanto no Android quanto no iOS.

Começaremos escrevendo a base da aplicação:

PayPal.mxml

 <?xml version="1.0" encoding="utf-8"?>
<s:ViewNavigatorApplication
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    firstView="com.paypal.example.views.Home"
    splashScreenImage="@Embed('splash.png')">

    <s:navigationContent>
        <s:Button click="navigator.popToFirstView()" icon="@Embed('paypal-icon_48x48.png')" />
    </s:navigationContent>

    <s:actionContent>
        <s:Button click="navigator.pushView(com.paypal.example.views.ShoppingCart)" icon="@Embed('cart.png')" />
    </s:actionContent>
</s:ViewNavigatorApplication>

Nossa primeira View é a Home, onde exibiremos os produtos ao usuário:

Home.mxml

 <?xml version="1.0" encoding="utf-8"?>
<s:View
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx"
    title="Produtos">

    <fx:Script>
    <![CDATA[
        import spark.components.DataGroup;
        import com.paypal.example.Application;
        import com.paypal.example.Cart;

        private var application :Application = Application.instance();

        private var cart :Cart = application.cart();

        private function buyNow() :void {
            checkoutButton.enabled = false;

            cart.addEventListener( "SUCCESS" , function() :void {
                checkoutButton.enabled = true;
                navigator.pushView( com.paypal.example.views.PayPalCheckout , cart.token );
                cart.clean();
            } );

            cart.addEventListener( "OPZ" , function() :void {
                checkoutButton.enabled = true;
            } );

            cart.buyNow( list.selectedItem )
        }

        private function scrollList() :void {
            var index :int = list.selectedIndex;

            if ( ( index != -1 ) && ( list.layout != null ) ) {
                var target <img src="https://www.paypal-brasil.com.br/x/wp-includes/images/smilies/icon_biggrin.gif" alt=":D" class="wp-smiley"> ataGroup = list.dataGroup;
                var spDelta:Point = target.layout.getScrollPositionDeltaToElement( index );

                if ( ( spDelta != null ) && ( spDelta.x != 0 ) ) {
                    pth.valueBy = spDelta.x;
                    animation.play();
                }
            }
        }
    ]]>
    </fx:Script>

    <fx:Declarations>
        <mx:CurrencyFormatter
            id="currencyFormatter"
            currencySymbol="R$"
            precision="2"
            decimalSeparatorFrom=","
            decimalSeparatorTo=","
            useNegativeSign="true"
            useThousandsSeparator="false"
            alignSymbol="left" />

        <s:Animate id="animation" target="{listLayout}" duration="100">
            <s:motionPaths>
                <s:SimpleMotionPath id="pth" property="horizontalScrollPosition" />
            </s:motionPaths>
        </s:Animate>

        <fx:XML id="products" source="http://189.90.143.178/pp/products.xml" />
    </fx:Declarations>

    <s:VGroup
        width="100%"
        height="100%"
        horizontalAlign="center">

        <s:List
            id="list"
            itemRenderer="com.paypal.example.renderers.ProductRenderer"
            top="0"
            left="0"
            width="100%"
            height="400"
            dragMoveEnabled="true"
            borderVisible="false"
            selectedIndex="0"
            enabled="true"
            change="scrollList()"
            contentBackgroundColor="0xFFFFFF">

            <s:layout>
                <s:HorizontalLayout
                    id="listLayout"
                    gap="0"
                    paddingLeft="0"
                    paddingRight="0"
                    paddingTop="0"
                    paddingBottom="0"
                    verticalAlign="top" />
            </s:layout>

            <s:dataProvider>
                <s:XMLListCollection source="{products.children()}" />
            </s:dataProvider>
        </s:List>

        <s:Label
            text="{list.selectedItem.name}"
            fontSize="30"
            fontWeight="bold"
            color="0x013262" />

        <s:HGroup
            width="100%"
            paddingTop="10"
            horizontalAlign="center">

            <s:Button
                chromeColor="0x013262"
                color="0xFFFFFF"
                click="navigator.pushView( com.paypal.example.views.ProductDetails , list.selectedItem );"
                enabled="{list.selectedItem != null}"
                label="Visualizar" />

            <s:Button
                chromeColor="0x013262"
                color="0x33FF33"
                click="application.cart().add( list.selectedItem )"
                enabled="{list.selectedItem != null}"
                label="Adicionar" />
        </s:HGroup>

        <s:Button
            id="checkoutButton"
            skinClass="com.paypal.mobile.CheckoutButton"
            enabled="{list.selectedItem != null}"
            click="buyNow()"
            label="Comprar Agora" />
    </s:VGroup>
</s:View>

É nessa View que toda a mágica começa, é aqui que exibimos os produtos ao usuário e permitimos que ele compre diretamente um produto, clicando em “Comprar Agora” ou então damos a ele a possibilidade de adicionar um produto ao carrinho.

Como podem ter visto, temos também um botão “Visualizar”, que permitirá que o usuário saiba mais sobre um produto, veja descrições etc.

ProductDetails.mxml

 <?xml version="1.0" encoding="utf-8"?>
<s:View
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx"
    creationComplete="init()"
    title="{data.name}">

    <fx:Script>
    <![CDATA[
        import com.paypal.example.Application;
        import com.paypal.example.Cart;

        private var application :Application = Application.instance();
        private var cart :Cart = application.cart();

        private function init() :void {
            application.loadImage( data.img.@src.toString() , image );
        }

        private function buyNow() :void {
            checkoutButton.enabled = false;

            cart.addEventListener( "SUCCESS" , function() :void {
                checkoutButton.enabled = true;
                navigator.pushView( com.paypal.example.views.PayPalCheckout , cart.token );
                cart.clean();
            } );

            cart.addEventListener( "OPZ" , function() :void {
                checkoutButton.enabled = true;
            } );

            cart.buyNow( XML( data)  );
        }
    ]]>
    </fx:Script>

    <fx:Declarations>
        <mx:CurrencyFormatter
            id="currencyFormatter"
            currencySymbol="R$"
            precision="2"
            decimalSeparatorFrom=","
            decimalSeparatorTo=","
            useNegativeSign="true"
            useThousandsSeparator="false"
            alignSymbol="left" />
    </fx:Declarations>

    <s:VGroup
        width="100%"
        height="100%"
        horizontalAlign="center"
        verticalAlign="middle"
        paddingBottom="10">

        <s:BitmapImage
            id="image"
            source="img/app/loader_450x400.gif"
            width="450"
            height="400" />

        <s:Label
            text="{data.name.toString()}"
            fontSize="30"
            fontWeight="bold" />

        <s:Label
            text="{currencyFormatter.format(data.price)}"
            fontSize="20" />

        <s:Group width="100%" height="100%">
            <s:Label
                top="0"
                left="10"
                text="Descrição:"
                fontWeight="bold" />

            <s:TextArea
                top="20"
                left="10"
                right="10"
                bottom="10"
                contentBackgroundColor="0xFFFFFF"
                borderVisible="false"
                editable="false"
                text="{data.description}" />
        </s:Group>

        <s:HGroup
            width="100%"
            horizontalAlign="center"
            verticalAlign="middle">

            <s:Button
                chromeColor="0x013262"
                color="0x33FF33"
                click="application.cart().add( XML( data ) );"
                label="Adicionar" />

            <s:Button
                id="checkoutButton"
                skinClass="com.paypal.mobile.CheckoutButton"
                click="buyNow()"
                label="Comprar Agora" />
        </s:HGroup>
    </s:VGroup>
</s:View>

Ao contrário da View principal, não temos mais um botão “Visualizar”, claro, dessa vez temos apenas o botão “Comprar Agora” e o botão “Adicionar”, que colocará um produto no carrinho.

O carrinho é bastante simples:

ShoppingCart.mxml

 <?xml version="1.0" encoding="utf-8"?>
<s:View
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx"
    title="Meu Carrinho">

    <fx:Script>
    <![CDATA[
        import com.paypal.example.Application;
        import com.paypal.example.Cart;

        [Bindable]
        private var cart :Cart = Application.instance().cart();

        [Bindable]
        private var products :XML = cart.getItems();

        private function checkout() :void {
            checkoutButton.enabled = false;

            cart.addEventListener( "SUCCESS" , function() :void {
                checkoutButton.enabled = true;
                navigator.pushView( com.paypal.example.views.PayPalCheckout , cart.token );
                cart.clean();
            } );

            cart.addEventListener( "OPZ" , function() :void {
                checkoutButton.enabled = true;
            } );

            cart.checkout();
        }

        private function clean() :void {
            cart.clean();
            navigator.popToFirstView();
        }
    ]]>
    </fx:Script>

    <fx:Declarations>
        <mx:CurrencyFormatter
            id="currencyFormatter"
            currencySymbol="R$"
            precision="2"
            decimalSeparatorFrom=","
            decimalSeparatorTo=","
            useNegativeSign="true"
            useThousandsSeparator="false"
            alignSymbol="left" />
    </fx:Declarations>

    <s:VGroup
        width="100%"
        height="100%"
        paddingBottom="20"
        paddingTop="10"
        horizontalAlign="center"
        verticalAlign="top">

        <s:List
            id="list"
            itemRenderer="com.paypal.example.renderers.CartItemRenderer"
            width="100%"
            height="100%"
            borderVisible="false"
            selectedIndex="0"
            enabled="true"
            contentBackgroundColor="0xFFFFFF">

            <s:dataProvider>
                <s:XMLListCollection source="{products.children()}" />
            </s:dataProvider>
        </s:List>

        <s:HGroup
            right="10"
            height="40"
            verticalAlign="middle">

            <s:Label text="TOTAL:" />
            <s:Label
                text="{currencyFormatter.format(cart.total)}"
                fontWeight="bold"
                color="0x333333" />
        </s:HGroup>

        <s:HGroup
            width="100%"
            horizontalAlign="center"
            verticalAlign="middle">

            <s:Button
                chromeColor="0xFF3333"
                color="0xFFFFFF"
                click="clean()"
                label="Limpar" />

            <s:Button
                id="checkoutButton"
                click="checkout()"
                skinClass="com.paypal.mobile.CheckoutButton"
                label="Fazer Checkout com" />
        </s:HGroup>
    </s:VGroup>
</s:View>

Temos um botão “Limpar” para que o cliente possa se desfazer da seleção e o botão “Fazer checkout com PayPal”.

Já na view de checkout, utilizaremos uma WebView para permitir que o usuário acesse o ambiente seguro do PayPal sem sair de nossa aplicação.

Para isso, abriremos uma “porta de visualização” e monitoraremos os redirecionamentos para saber quando o processo foi concluído:

PayPalCheckout.mxml

 <?xml version="1.0" encoding="utf-8"?>
<s:View
    xmlns:fx="http://ns.adobe.com/mxml/2009"
    xmlns:s="library://ns.adobe.com/flex/spark"
    xmlns:mx="library://ns.adobe.com/flex/mx"
    creationComplete="init()"
    title="PayPal Checkout">

    <fx:Script>
    <![CDATA[
        private function init() :void {
            var webView :StageWebView = new StageWebView();
            webView.stage = this.stage;
            webView.viewPort = new Rectangle(
                0,
                78,
                stage.stageWidth,
                stage.stageHeight
            );

            webView.loadURL( "https://www.sandbox.paypal.com/br/cgi-bin/webscr?cmd=_express-checkout-mobile&token=" + data );
            webView.addEventListener( LocationChangeEvent.LOCATION_CHANGING , function( e :LocationChangeEvent ) :void {
                e.preventDefault();
                webView.loadURL( e.location );
            } );

            webView.addEventListener( LocationChangeEvent.LOCATION_CHANGE , function( e :LocationChangeEvent ) :void {
                var lastSlash :Number = e.location.lastIndexOf( "/" );

                if ( e.location.indexOf( "retorno.php" ) >= 0 ) {
                    webView.dispose();
                    webView = null;

                    navigator.pushView( com.paypal.example.views.Complete );
                } else if ( e.location.indexOf( "cancelamento.php" ) >= 0 ) {
                    webView.dispose();
                    webView = null;

                    navigator.pushView( com.paypal.example.views.Canceled );
                }
            } );
        }
    ]]>
    </fx:Script>
</s:View>

Abaixo alguns screenshots da aplicação funcionando no emulador:

A aplicação e o código completo podem ser encontrados em Code Sample

Mensagem do anunciante:

Receba consultoria especializada em WordPress com os melhores profissionais do mercado. Conheça o Apiki WP Consultoria.

Qual a sua opinião?