Monday, November 18, 2019

Paypal Checkout with Smart Payment Buttons

Paypal is globally used electronic payment processor. People can shop online, transfer money online and receive money online using a single paypal account. Because of its features and global reach paypal has become a "must be included" payment method for online shopping stores.

Paypal Checkout with Smart Payment Buttons
Paypal had provided API integrations for online shopping stores to receive payments for placed orders. Recently paypal has updated orders API and introduced Smart Payment Buttons for checkout and recommends using this new API integration which uses Javascript SDK in combination with Orders  API v2. This Smart Payment Buttons is valid from Feb, 2019. Paypal is not going to update old checkout API which uses checkout.js for new features and enhancements. So it is recommended to use the new checkout integration using Smart Payment Buttons.

How do Smart Payment Buttons work?

  1. You place the buttons on checkout page on your website.
  2. Buyers clicks the buttons to pay for order.
  3. Buttons launch the checkout experience.
  4. Buyers makes the payment and Orders API is called to finalize the transaction.
  5. You show confirmation to buyer upon successful transaction.

How to setup Paypal Smart Payment Buttons?

Smart Payment Buttons can be either client side integration or server side integration. We will implement these buttons using server side approach as it does not reveal much information to client and we can call Paypal's order api from our server. You can refer to paypals Call Orders API from Your Server for use cases which request you are supposed to make depending upon your requirements. Lets break it into following steps:
  • We will need a paypal sandbox account.
  • We will use OAuth 2.0 client ID and client secret of this sandbox account.
  • We will implement Payment Buttons using Javascript SDK.
  • Setup our server to make calls to Paypal's Orders API v2.
  • We will call Paypal Orders API from our server, to create an order and capture the transaction.
Before we get started, For demonstration I have created a session to hold cart products and amount statically. I shall not be saving order to database after successful transaction and I am under opinion you already have a cart setup. So lets get started

Create a Sandbox App and Get Rest Credentials

  • Login to your paypal account and navigate to paypal developer
  • Click on accounts in left navigation menu under Sandbox.
  • Click on Create Account button, We need two accounts one for buyer and other for merchant. You don't need to create these if you already have these sandbox accounts.
  •  Now click on App & Credentials in left navigation menu under dashboard.
  • Click on Create App and enter App name and assign a merchant account to App

    Paypal App & Credentials
  • After you have created your app you will see a screen like this copy Client ID and Client Secret.

    Paypal Client ID & Credentials
Now that we have our Sandbox Client ID & Secret, we are ready to setup our Smart Payment Buttons. First install Paypal Server SDK using following command in composer.
composer require paypal/paypal-checkout-sdk 1.0.0
We are going to create following files in the directory where our checkout cart is placed.
  • constants.php
  • index.php
  • javascript.js
  • paypal-request.php
  • style.css

constants.php

<?php
define("PAYPAL_CLIENT_ID", "YOUR_CLIENT_ID");
define("PAYPAL_CLIENT_SECRET","YOUR_CLIENT_SECRET");
?>

index.php

Contains our checkout cart, I have created a static array of products for demonstration. Next loop through products array to calculate total tax amount and total items amount (without tax). Then I created a cart session that can be accessed in our Paypal SDK.
<?php
include("constants.php");

$products = [];

//=== Static array of products
$products[] = ["product_title" =>  "Javascript Book - PDF", "product_price" => 10.00, "product_tax" => 2.00, "product_quantity"  => 3];
$products[] = ["product_title" =>  "PHP Book - PDF", "product_price" => 15.00, "product_tax" => 3.00, "product_quantity"  => 2];

$items_total = $tax_total = 0.00;

//=== Loop through products and calculate tax total & items total
foreach($products as $product){
    $tax_total += floatval($product["product_tax"] * $product["product_quantity"]);
    $items_total += floatval($product["product_price"] * $product["product_quantity"]);
}

//=== Function to show amount in price format
function to_price($amount = 0.00, $symbol = "$", $currency_code = "USD"){
    $amount = floatval($amount);
    return sprintf("%s%.2f %s", $symbol, $amount, $currency_code);
}

//=== Create a cart session
$_SESSION["cart"]["products"] = $products;
$_SESSION["cart"]["items_total"] = $items_total;
$_SESSION["cart"]["tax_total"] = $tax_total;
$_SESSION["cart"]["cart_total"] = $tax_total + $items_total;

?>
<!DOCTYPE html>
<html>
    <head>
        <title>Paypal Checkout with Smart Payment Buttons - Demo</title>
        <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
        <script defer="defer" src="https://www.paypal.com/sdk/js?client-id=<?php echo PAYPAL_CLIENT_ID?>"></script>
        <script defer="defer" type="text/javascript" src="js/javascript.js"></script>
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <div class="main-container">
            <div class="section">
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th colspan="2">Product</th>
                            <th width="120">Price</th>
                            <th width="120">Tax</th>
                            <th width="100">Quantity</th>
                            <th width="120">Item Total</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach($_SESSION["cart"]["products"] as $product){
                            $item_total = $product["product_price"] * $product["product_quantity"];
                            $item_tax = $product["product_tax"] * $product["product_quantity"];
                            ?>
                            <tr>
                                <td width="31"><img src="images/pdf-book.png" /></td>
                                <td>
                                    <?php echo $product["product_title"];?>
                                </td>
                                <td><?php echo to_price($product["product_price"]);?></td>
                                <td><?php echo to_price($product["product_tax"]);?></td>
                                <td><?php echo $product["product_quantity"]?></td>
                                <td><?php echo to_price($item_total + $item_tax);?></td>
                            </tr>
                        <?php }?>
                        <tr>
                            <th colspan="5" class="text-right">
                                Tax Total
                            </th>
                            <th class="text-right">
                                <?php echo to_price($_SESSION["cart"]["tax_total"]);?>
                            </th>
                        </tr>
                        <tr>
                            <th colspan="5" class="text-right">
                                Items Total
                            </th>
                            <th class="text-right">
                                <?php echo to_price($_SESSION["cart"]["items_total"]);?>
                            </th>
                        </tr>
                        <tr>
                            <th colspan="5" class="text-right">
                                Cart Total
                            </th>
                            <th class="text-right">
                                <?php echo to_price($_SESSION["cart"]["cart_total"]);?>
                            </th>
                        </tr>
                    </tbody>
                </table>
                <div id="paypal-buttons"></div>
            </div>
        </div>
    </body>
</html>

javascript.js

Renders smart payment buttons, "createOrder" initiates a call to our server side SDK to create an order. "onApprove" is invoked when transaction is successful and we redirect to a thank you/success page after successful transaction.
window.addEventListener("load", function(){
    //=== Render paypal Buttons
    paypal.Buttons({
        style:{
            layout: "horizontal"
        },
        //=== Call your server to create an order
        createOrder: function(data, actions) {
            return fetch("paypal-request.php", {
                mode: "no-cors",
                method: "POST",
                headers: {
                    "content-type": "application/json"
                },
                body: JSON.stringify({
                    action: "create-order",
                })
            }).then(function(res) {
                return res.json();
            }).then(function(data) {
                if( !data.success && data.message ){
                    console.error(data.message);
                }
                return data.id;
            });
        },
        //=== Call your server to save the transaction
        onApprove: function(data, actions){
            return fetch("paypal-request.php", {
                mode: "no-cors",
                method: "POST",
                headers: {
                    "content-type": "application/json"
                },
                body: JSON.stringify({
                    action: "save-order",
                    id: data.orderID
                })
            }).then(function(res) {
                return res.json();
            }).then(function(data){
                //=== Redirect to thank you/success page after saving transaction
                if(data.success){
                    window.location.assign("payment-success.php");
                }
            });
        }
    }).render("#paypal-buttons");
});

paypal-request.php

Loads SDK client library to handle the calls to paypal and receives response.
environment (method): Creates our sandbox environment.
client (method): Setup the paypal client for authorization.
buildRequest (method): Loops through all products in session and prepares an array of order to be passed to paypal to create order.
createOrder (method): Calls paypal orders to create an order sending the request which is prepared in buildRequest (method) as parameter.
captureOrder(method): Captures the paypal order against given order ID.
<?php
session_start();
include("constants.php");

//=== Import the PayPal SDK client that was created in `Set up Server-Side SDK`.
require __DIR__ . '/vendor/autoload.php';

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;

class paypalRequest
{
    //=== Setup Paypal Environment.
    public static function environment()
    {
        return new SandboxEnvironment(PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET);
    }

    //=== Setup Paypal Client.
    public static function client()
    {
        return new PayPalHttpClient(self::environment());
    }

    //=== Create papal order
    public static function createOrder( $debug = false )
    {
        //=== Create New Order Request
        $request = new OrdersCreateRequest();
        $request->prefer("return=representation");

        //=== Create Request Body
        $request->body = self::buildRequestBody();

        //=== Call PayPal to set up a transaction
        $client = self::client();
        $response = $client->execute($request);

        //=== Return a response to the client.
        return $response;
    }

    public static function captureOrder($order_id)
    {
        $request = new OrdersCaptureRequest($order_id);

        //=== Call PayPal to get the transaction details
        $client = self::client();
        $response = $client->execute($request);

        return $response;
    }

    private static function buildRequestBody()
    {
        //=== Graceful die if cart is not set in session
        if( !isset($_SESSION["cart"]) ){
            die(json_encode(["success" => 0, "message" => "No items found in cart"]));
        }
        //=== Assign cart from session to a variable
        $cart = $_SESSION["cart"];
        //=== Set currency code used in transaction
        $currency_code = "USD";

        $request_body = $application_context = $purchase_units = $pu_amount = [];

        //=== Prepare context of our request
        $application_context["locale"] = "en-US";
        $application_context["user_action"] = "PAY_NOW";

        //=== Prepare amount & break down of amount for order
        $pu_amount["currency_code"] = $currency_code;
        $pu_amount["value"] = floatval($cart["cart_total"]);

        //=== Items total break down without tax
        $pu_amount["breakdown"]["item_total"]["currency_code"] = $currency_code;
        $pu_amount["breakdown"]["item_total"]["value"] = number_format($cart["items_total"], 2);

        //=== Total tax of all products
        $pu_amount["breakdown"]["tax_total"]["currency_code"] = $currency_code;
        $pu_amount["breakdown"]["tax_total"]["value"] = number_format($cart["tax_total"], 2);

        $purchase_units["amount"] = $pu_amount;

        $items = [];

        //=== Loop through all products in session and prepare items array for order request
        if( !empty($cart["products"]) ){
            foreach($cart["products"] as $product){
                $item["name"]= $product["product_title"];
                $item["quantity"]= $product["product_quantity"];
                $item["category"]= "DIGITAL_GOODS";

                //=== Unit amount of product (widthout tax)
                $item["unit_amount"]["currency_code"] = $currency_code;
                $item["unit_amount"]["value"]= number_format($product["product_price"], 2);

                $items[] = $item;
            }
        }

        $purchase_units["items"] = $items;

        //=== Finally create request body array assigning context & purchase units created above
        $request_body["intent"] = "CAPTURE";
        $request_body["application_context"] = $application_context;
        $request_body["purchase_units"][] = $purchase_units;

        return $request_body;
    }
}

if ( !count( debug_backtrace() ) )
{
    //=== Capture the input received from fetch() request
    $json_data = file_get_contents("php://input");
    $post = json_decode($json_data);

    //=== Check if request is to create an order
    if($post->action == "create-order")
    {
        $order = paypalRequest::createOrder();
        echo json_encode($order->result, JSON_PRETTY_PRINT);
    }
    //=== Check if request is to save an order
    elseif($post->action == "save-order")
    {
        //=== Save order either by retrieving order from paypal OR the order we still have in session
        $order = paypalRequest::captureOrder($post->id);

        /*
        Prepare order query data & save order into database here
        */
        //===  unset cart session
        unset($_SESSION["cart"]);

        echo json_encode(["success" => 1], JSON_PRETTY_PRINT);
    }
}
?>

style.css

*{
    box-sizing: border-box;
}
html,body{
    margin: 0px;
    padding: 0px;
}
body{
    background: #f0f0f0;
    font: normal normal 14px Open Sans,Verdana, Arial;
}
.main-container{
    max-width: 1024px;
    margin: 0px auto;
}
.section {
    padding: 15px;
}

.text-right{
    text-align: right;
}

.table{
    border: 1px solid #dddddd;
    width: 100%;
    border-collapse: collapse;
    margin: 5px 0px;
    background: #fff;
}

.table tr th,
.table tr td{
    border: 1px solid #dddddd;
    padding: 10px;
}

#paypal-buttons{
    max-width: 300px;
    margin: 10px 0px 0px auto;
}

Login to your sandbox buyer or merchant account and verify transaction was placed. You would see transaction details like in screenshot below:

Paypal Transaction Details
You can modify the code to avoid second request for creating order in database.