How to integrate Stripe Apple Web pay in to an existing Laravel E-Commerce app?

Commissioning the back end series

Created on: May 02, 2017

Updated on: April 25, 2018

Written by Sehinde Raji

E-commerce has moved on leaps and bounds over the past 10 years and it’s been interesting to see how the various payment methods have exploded on the scene recently. We can now use mobile phone payments to pay for public transport fares, e-commerce and parking.

My goal has been to incorporate the latest and greatest technology in to my applications in an effort to increase efficiency and take up of these methods.

A few months ago, I searched the web to look for ways in which I could incorporate Apple Web pay in to my application and I was astonished as to how challenging it could be to follow the WWDC instructions on incorporating apple pay in to my app.

The references to merchant certificates were cumbersome and extremely challenging.

Thankfully Stripe has saved the day and they have come up with a wonderful solution to incorporating Apple Web pay in to your app.

As with most tutorials we have to make a number of assumptions from the outset. The reason being that setting up an E-Commerce site from scratch can take a considerable amount of time.

Later on, in the year we will summarise how to set up an ecommerce site from scratch within a future article. We will concentrate on adding Stripe Apple Web pay to our existing gamesstation app.

Assumptions:

1. Cart Controller this will handle the order process for the shopping cart if the user selects add to cart on the product show page.

2. Checkout Controller this will handle the checkout process after stripe checkout has charged the credit card and handed back the stripe token after an AJAX call.

3. Products Controller this will handle the product listing, storage, amendment and the deletion of all new products.

4. Please remember that Apple pay requires all users to have a website and domain, running on a hosted server.

5. Apple pay will not work on a local machine running Laravel Homestead or Laravel Valet.

Our starter source code (i.e. it doesn’t contain any references to apple pay) can be found at the link listed below:

Starter code

We strongly recommend that you pull down or fork, the repository before you embark on the tutorial as this will provide you with the tools to complete the job.

Prerequisites

1. Laravel Valet

2. Laravel 5.4

3. Laravel Forge

4. Javascript

5. Safari

6. Chrome

7. Apple Pay Developer Account

8. Macos Sierra

9. IOS10

Listings

The product show page is the page that determines the various payment methods that are on offer at any one time. The gamesstation project contains a show page and this is illustrated within the image listed below:

resources/views/show.blade.php:

minipic

In order for us to set the context of our project we need to show you a listing of all the views and controllers so that you can gain a birds eye view of our project.

Lets start with the products controller:

productscontroller.php
public function show($id)
{
    $product = Product::findOrFail($id);

    return view('admin.products.show', compact('product'));
}
app\resources\views\show.blade.php
@extends('layouts.format')

@section('meta-title', $product->title)

@section('content')
<div class="product-fluid">
  <div class="left-pane">
       <div class="window">
          <img class="product-img" src="/images/products/{{ $product->sku }}.png"/>
        </div><!-- /.window -->

            <div class="product-image">

            </div><!-- /.product-image -->
    </div><!-- /.left-pane -->

    <div class="right-pane">
         <div class="new">
             <h4 class="is--centered">New</h4>
         </div><!-- /.new -->

         <div class="detail-panel">
             <div class="show-heading">
                <h1>{{ $product->name }}</h1>
                <h5>£{{ $product->price }}</h5>
             </div><!-- /.show-heading -->
             <hr/>
         <p class="--centre is--padded-bottom-ten">{{ $product->description }}</p>

     {!! Form::open(array('url' => '/checkout')) !!}
     {!! Form::hidden('product_id', $product->id) !!}
       <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
         data-key="{{ env('STRIPE_KEY') }}"
         data-name="Gamestation Ltd"
         data-billing-address=true
         data-shipping-address=true
         data-label="Buy £{{ $product->price }}"
         data-description="{{ $product->name }}"
         data-amount="{{ $product->price * 100 }}"
         data-currency="gbp">
       </script>
     {!! Form::close() !!}

     {!! Form::open(['url' => '/cart/store']) !!}
         <input type="hidden" name="product_id" value="{{ $product->id }}"/>
         <button type="submit" class="btn btn-cart">Add to Cart</button>
     {!! Form::close() !!}
         <h2 class="sub-heading --centre">Click to edit ? {!! link_to_route('admin.products.edit', $product->name, [$product->id]) !!}</h2>
                        <hr/>
        </div><!-- /.detail-pane -->
    </div><!-- /.right-pane -->
</div><!-- /.product-fluid -->

<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
    {!! Form::open(array('url' => '/checkout/charges/')) !!}
    {!! Form::hidden('product_id', $product->id) !!}
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    {!! Form::close() !!}
<script>Stripe.setPublishableKey("<?php echo env('STRIPE_KEY') ?>");</script>
@endsection
app\http\controllers\checkoutcontroller.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Events\CheckoutWasCompleted;
use App\Events\CheckoutWasIncomplete;
use App\Mail\DigitalDownload;
use App\Order;
use App\Product;
use App\User;
use Carbon\Carbon;
use Mail;
//use Stripe\Stripe;

class CheckoutController extends Controller
{
    public function __construct()
    {

        parent::__construct();
    }

        public function index(Request $request)
    {

        $user = new User();

        $products = Product::find($request->input('product_id'));

        $stripeEmail = $request->input('stripeEmail');

        $stripeToken = $request->input('stripeToken');

        if($user->charge($products->priceToCents(),
            [
                'source' => $stripeToken,
                'receipt_email' => $stripeEmail
            ])
        ) {

            $orders = new Order();
     // Generate random orders number
            $orders->order_number = substr(md5(microtime()), rand(0, 20), 6) . time();

            $orders->product_id = $products->id;

            $orders->email = $request->input('stripeEmail');
            $orders->billing_name = $request->input('stripeBillingName');
            $orders->billing_address = $request->input('stripeBillingAddressLine1');
            $orders->billing_city = $request->input('stripeBillingAddressCity');
            $orders->billing_zip = $request->input('stripeBillingAddressZip');
            $orders->billing_country = $request->input('stripeBillingAddressCountry');

            $orders->shipping_name = $request->input('stripeShippingName');
            $orders->shipping_address = $request->input('stripeShippingAddressLine1');
            $orders->shipping_city = $request->input('stripeShippingAddressCity');
            $orders->shipping_zip = $request->input('stripeShippingAddressZip');
            $orders->shipping_country = $request->input('stripeShippingAddressCountry');

            $orders->save();

            if ($orders->product->is_downloadable) {

                $orders->onetimeurl = md5(time() . $orders->email . $orders->order_number);

                $orders->save();

                $when = Carbon::now()->addMinutes(10);

                Mail::to($orders->email)->later($when, new DigitalDownload($orders));
            }

        } else {

            event(new CheckoutWasIncomplete($user));

            return redirect()->route('products.show', [$products->id]);
        }

        $user = User::first();

        event(new CheckoutWasCompleted($user));

        return redirect()->route('checkout.thankyou');

    }

     public function thankyou()
     {

        return view('checkout.thankyou');

     }
}

Front end styling

We need to create a style tag, that will contain the styling for the buttons that we will use on our show.blade.php.

Here is the style tag we will use:

resources/views.show.blade.php
<style>
  #apple-pay-button {
                     width: 280px;
                     height: 64px;
                     display: inline-block;
                     box-sizing: border-box;
                     background-image: url(/images/[email protected]);
                     background-size: 100%;
                     background-repeat: no-repeat;
                    }
</style>

In our case this will be placed just underneath the hr tag underneath the cart declaration as shown in the snippet below:

resources/views.show.blade.php

{!! Form::open(['url' => '/cart/store']) !!}
<input type="hidden" name="product_id" value="{{ $product->id }}"/>
<button type="submit" class="btn btn-cart">Add to Cart</button>
{!! Form::close() !!}

<h2 class="sub-heading --centre">Click to edit ? {!! link_to_route('admin.products.edit', $product->name, [$product->id]) !!}</h2>
<hr/>
<style>
    #apple-pay-button {
        width: 280px;
        height: 64px;
        display: inline-block;
        box-sizing: border-box;
        background-image: url(/images/[email protected]);
        background-size: 100%;
        background-repeat: no-repeat;
    }
</style>

Meanwhile, you are more than welcome to add the above-mentioned style tag to your overall global stylesheet's wherever they are used within your application.

Button Import

The next stage is to import the button that we will use to show on the show.blade.php and to do this you must have an Apple Pay Developer account to download the respective images.

Go to https://developer.apple.com/

Apple Developer: Apple Developer site

Copy the applepayBTN and place it within public/images folder.

Terms and Conditions

Meanwhile, the next stage is to add the terms and conditions to our button.

Notice that the links contain respective classes these are responsible for showing or hiding the buttons on different browsers.

resources\views\show.blade.php

{!! Form::open(['url' => '/cart/store']) !!}
<input type="hidden" name="product_id" value="{{ $product->id }}"/>
<button type="submit" class="btn btn-cart">Add to Cart</button>
{!! Form::close() !!}
<h2 class="sub-heading --centre">Click to edit ? {!! link_to_route('admin.products.edit', $product->name, [$product->id]) !!}</h2>
<hr/>
<style>
    #apple-pay-button {
        width: 280px;
        height: 64px;
        display: inline-block;
        box-sizing: border-box;
        background-image: url(/images/[email protected]);
        background-size: 100%;
        background-repeat: no-repeat;
    }
</style>

<button id="apple-pay-button" style="display:none"></button>
  <p style="display:none" id="notgot">ApplePay is not available with this browser</p>
<div id="apple-link" style="display:none">
    <li><a href="http://www.apple.com/uk/privacy/" class="terms-link">Apple Pay Terms and Conditions</a></li>
 </div><!-- /.apple-link -->

How does it work?

Apple pay works by running the script asynchronously (in the background) this means that the user of the application isn’t aware of the processes until an error is recorded within the console log.

Package our data

We will create a form and this is where we will pass the product id and the csrf token as hidden fields the amounts are totalled, wrapped and sent to Stripe for charging.

Let’s add this functionality to the product fluid div:

<style>
   #apple-pay-button {
       width: 280px;
       height: 64px;
       display: inline-block;
       box-sizing: border-box;
       background-image: url(/images/[email protected]);
       background-size: 100%;
       background-repeat: no-repeat;
        }
</style>
</div><!-- /.detail-pane -->
</div><!-- /.right-pane -->
</div><!-- /.product-fluid -->

<button id="apple-pay-button" style="display:none"></button>
  <p style="display:none" id="notgot">ApplePay is not available with this browser</p>
<div id="apple-link" style="display:none">
  <li><a href="http://www.apple.com/uk/privacy/" class="terms-link">Apple Pay Terms and Conditions</a></li>
</div><!-- /.apple-link -->

<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
    {!! Form::open(array('url' => '/checkout/charges/')) !!}
    {!! Form::hidden('product_id', $product->id) !!}
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    {!! Form::close() !!}
<script>Stripe.setPublishableKey("<?php echo env('STRIPE_KEY') ?>");</script>

Please note that we have added the Stripe publishable key as a separate script. Stripe needs our key to successfully authenticate against the Stripe Api.

CSRF token middleware exclusions

As part of our processing we need to make sure that we add the CSRF token as an exclusion. If this isnt performed it will prevent our stripe transaction from completing successfully.

Go to app\http\middleware\verifyCSRFToken.php

class VerifyCsrfToken extends BaseVerifier
{
        protected $except = [
        '/checkout/charges/*',
        'stripe/*'

    ];
}

Add the checkout/charges and stripe exclusions within the protected except section as illustrated above.

Displaying the Apple pay button

Lets take a look back at our "package our data" section and notice that we added a new script containing the stripe key. We will be amending this section with a special function called check availability and it is within this method where we will display the button only on safari browsers.

Check Availability

We will amend this script and we will add the stripe apple pay check availability method to it to ensure that the apple pay button is only displayed on the safari browser.

This will be evaluated by css classes and if the evaluation is successful?

We will pass the price and product variables using php and this will invoke the beginApplePay function by instantiating a payment request.

The required billing fields are passed in and a session is invoked and the payment token is returned from stripe.

This is illustrated within the script below:

<script>

  Stripe.setPublishableKey("<?php echo env('STRIPE_KEY') ?>");

                        Stripe.applePay.checkAvailability(function(available) {

  if (available) {
     document.getElementById('apple-pay-button').style.display = 'block';
     document.getElementById('apple-link').style.display = 'block';
     console.log('hi, I can do ApplePay');
   } else {
     document.getElementById("notgot").style.display = "block";
     console.log('ApplePay is possible on this browser, but not currently activated.');
   }
  });

     document.getElementById('apple-pay-button').addEventListener('click', beginApplePay);

     var price ="{{ ($product->price) }}";
     var id ="{{($product->id) }}";

 function beginApplePay() {
     var paymentRequest = {
     requiredBillingContactFields: ['postalAddress'],       requiredShippingContactFields: ['phone'],
                                countryCode: 'GB',
                                currencyCode: 'GBP',
                                total: {
                                    label: 'Ormrepo',
                                    amount: price
                                }
                            };

     var session = Stripe.applePay.buildSession(paymentRequest,
     function(result, completion) {
     //console.log(result.token.card.address_line1);
     $.post('/checkout/charges/{id}', { token: result.token.id, 
     price: "{{ ($product->price) }}", 
     id: "{{$product->id}}" }).done(function() {
                                    completion(ApplePaySession.STATUS_SUCCESS);
    // Prevent the form from submitting with the default action
    return false;
// You can now redirect the user to a receipt page, etc.
                                            window.location.href = '/success.html';

   }).fail(function() {
                                            completion(ApplePaySession.STATUS_FAILURE);

   });
    }, function(error) {

   console.log(error.message);
   });

   session.begin();
}
</script>

Note we have added some scaffolding below the payment request and this involves a success html page is instantiated when the charge is successful.

If the charge is unsuccessful we will show the errors using the console log.

Now we have reached the stage where we need to reflect the charge made on our backend server.

Backend Charges

Open up the checkout controller and add the charges function within here:

app\http\controllers\checkoutcontroller.php
    public function charges(Request $request)
    {

        Stripe::setApiKey("xxxxxxxxx");

        // Token is created using Stripe.js or Checkout!
        // Get the payment token submitted by the form:

        $id = $_POST['id'];
        $raw_price = $request->get('price');
        $price = ($raw_price * 100);

        $user = new User();
        $product = Product::findOrFail($id);

        if($user->charge($product->priceToCents(),
            [
                'source' => $request->get('token'),
                'amount' => $price,
                'receipt_email' => $user->email
            ])
        ) {

            $orders = new Order();
            // Generate random orders number
            $orders->order_number = substr(md5(microtime()), rand(0, 20), 6) . time();

            $orders->product_id = $product->id;

            $user = auth()->user();

            $orders->email = $user->email;

            $orders->billing_name = $user->name;
            $orders->billing_address = '10 Apple Street';
            $orders->billing_city = 'Appleton';
            $orders->billing_zip = 'AP1 8YT';
            $orders->billing_country = 'United Kingdom';

            $orders->shipping_name = $user->name;
            $orders->shipping_address = '10 Apple Street';
            $orders->shipping_city = 'Appleton';
            $orders->shipping_zip = 'AP1 8YT';
            $orders->shipping_country = 'United Kingdom';

            $orders->save();

            if ($orders->product->is_downloadable) {

                $orders->onetimeurl = md5(time() . $orders->email . $orders->order_number);

                $orders->save();

                $when = Carbon::now()->addMinutes(10);

                Mail::to($orders->email)->later($when, new DigitalDownload($orders));

            } else {

                event(new CheckoutWasIncomplete($user));

                return redirect()->route('products.show', [$product->id]);
            }

            $user = User::first();

            event(new CheckoutWasCompleted($user));

            return redirect()->route('checkout.thankyou');
        }

    }

Explanation

We fetch the id and the raw_price from the request object and we multiply the raw_price by 100 to get the amount in pence.

A new user is instantiated and the product is fetched, the user is charged according to the amount calculated. Consequently a receipt email is dispatched and sent to the user’s email address.

What about the order ?

The order is saved and a one-time URL is generated and an email is sent with the corresponding digital download. Note that in our case the email is dispatched 10 minutes after it was generated.

Domain setup

The final piece of our jigsaw is to set up your domain before you go live.

Follow Step 4 here https://stripe.com/docs/apple-pay/web

It describes how you can upload your domain association file, make sure that you create a new folder underneath the public directory called .well-known and upload the file within there.

Upload all of the files from Github to Laravel Forge using git push.

The next stage is to test our application.

Testing

Create an Apple Pay Sandbox Test account by following the instructions listed below:

Sandbox

Add the test card number to your test account wallet.

Create various test purchases on your phone.

Remembering that if you receive errors such as payment not complete you will have to go to your application on Laravel Forge and examine the laravel.log file.

Usually payment not completed errors are the result of

1. CSRF ajax problems due to incorrect exclusions within Laravel middleware.

2. Incorrect environmental variables for example incorrect Stripe keys.

3. Failure to upload the domain association files.

4. Failure to upload a test card to the sandbox users test account.

5. Incorrect price definitions due to incorrect types used on the database schema.

If you encounter these problems please make sure that you check the variables on the backend using dd.

Remember test and retest each time and eventually you will have a fully functioning apple pay on your mobile and Macos sierra.

The source code for the completed website is located at

Completed Code

Always remember that running your own QA tests ensures that you understand your code more thoroughly.