Apple Pay on Web-Integration with Rails-Part 2

Apple Pay

We are working step-by-step to integrate payments in our web application through Apple Pay. This is the second post of the series of blog posts. The series consists of below posts:

  1. Register Merchant ID, domain, and generate certificates
  2. Create merchant session, show payment modal and authorize payment
  3. Decrypt Apple Pay JSON response

So far, we have registered our Merchant ID and domain on Apple Developer Console. We have also generated the certificates needed for generating a merchant session and for processing payments.

Here, we will use those certificates to generate the merchant session, show the payment modal to the customers, and handle various scenarios of authorising payments by the customers.

Create merchant session, show payment modal and authorize payment

Contents:

  1. Create merchant session
  2. Setup Apple Pay on customer iPhone
  3. Launch application

1 Create merchant session

1.1 Certificates

Put merchant_id.cer file and its corresponding “.pem” file in /certificates directory inside your Ruby on Rails application. DO NOT push the certificates to Version Control System e.g. for git, you can add them to .gitignore and put the files directly on your server.

1.2 Add Apple Pay button

Lets add a button on our payment page:

<!-- app/views/shops/payment.html.erb -->
<button id='applepay-btn'>Apple Pay</button>

You can design the button as you like. Some of the designs provided by Apple are available here.

1.3 Toggle button

Now, we need to show/hide the button based on whether user’s browser or credit cards support payments through Apple Pay:

// app/assets/javascripts/apple_pay.js
/**
* Returns a boolean value indicating if Apple Pay is supported
*/
function isApplePaySupported() {
  var unsupported = !window.ApplePaySession;
  if (!unsupported) {
    var promise = ApplePaySession.canMakePaymentsWithActiveCard(MERCHANT_ID);
    promise.then(function(canMakePayments) {
      return unsupported = unsupported || !!canMakePayments;
    });
  }
  return !unsupported;
};

/**
* This method is called when the page is loaded.
*/
document.addEventListener('DOMContentLoaded', () => {
  if (isApplePaySupported()) {
    $('#applepay-btn').on('click', applePayButtonClicked);
  } else {
    // Browser and/or user's cards do not support Apple Pay
  }
});

1.4 Apple Pay button clicked

Now, we will implement the logic for when Apple Pay button is clicked.

// app/assets/javascripts/apple_pay.js
/**
* Our entry point for Apple Pay interactions.
* Triggered when the Apple Pay button is pressed
*/
function applePayButtonClicked() {
  /**
  * Sample payment data
  */
  var paymentRequest =
    {
      countryCode: 'JP',
      currencyCode: 'JPY',
      total: {
        label: 'Purchase at MyStore',
        amount: 2000
      },
      supportedNetworks: ['masterCard', 'visa', 'discover', 'amex'],
      merchantCapabilities: ['supports3DS']
    };
  var session = new ApplePaySession(2, paymentRequest);

  /**
  * Makes an AJAX request to your application server with URL provided by Apple
  */
  function getSession(url) {
    return new Promise(function(resolve, reject) {
      var xhr = new XMLHttpRequest;
      var requestUrl = '/path/to/applepay/merchant/session';

      xhr.open('POST', requestUrl);

      xhr.onload = function() {
        if (this.status >= 200 && this.status < 300) {
          return resolve(JSON.parse(xhr.response));
        } else {
          return reject({
            status: this.status,
            statusText: xhr.statusText
          });
        }
      };

      xhr.onerror = function() {
        return reject({
          status: this.status,
          statusText: xhr.statusText
        });
      };

      xhr.setRequestHeader('Content-Type', 'application/json');

      return xhr.send(JSON.stringify({
        url: url
      }));
    });
  };

  /**
  * Merchant Validation
  * We call our merchant session endpoint, passing the URL to use
  */
  session.onvalidatemerchant = (event) => {
    const validationURL = event.validationURL;
    var promise = getSession(event.validationURL);
    promise.then(function(response) {
      session.completeMerchantValidation(response);
    });
  };

  /**
  * This is called when user dismisses the payment modal
  */
  session.oncancel = (event) => {
    // Re-enable Apple Pay button
  };
 
  /**
  * Payment Authorization
  * Here you receive the encrypted payment data. You would then send it
  * on to your payment provider for processing, and return an appropriate
  * status in session.completePayment()
  */
  session.onpaymentauthorized = (event) => {
    const payment = event.payment;
    // You can see a sample `payment` object in an image below.
    // Use the token returned in `payment` object to create the charge on your payment gateway.

    if (chargeCreationSucceeds) {
      // Capture payment from Apple Pay
      session.completePayment(ApplePaySession.STATUS_SUCCESS);
    } else {
      // Release payment from Apple Pay
      session.completePayment(ApplePaySession.STATUS_FAILURE);
    }
  };

  /**
  * This will show up the modal for payments through Apple Pay
  */
  session.begin();
}

1.4 Setup server

You also need to setup an end point on your server side to create merchant session using the URL returned by Apple. This end point would correspond to the path specified in function `getSession` above.

# app/controllers/apple_pay_merchants_controller.rb
require 'net/http'
require 'uri'

class ApplePayMerchantsController < ApplicationController
  CERTIFICATES_DIR = 'certificates'.freeze
  MERCHANT_ID_CERT_FILE = 'merchant_id.cer'.freeze
  MERCHANT_ID_PEM_FILE = 'MerchantIdCertificate.key.pem'.freeze   # or whatever name you chose when creating the .pem file for merchant_id.cer file

  def get_session
    uri = URI.parse(params[:url])
    http = build_http(uri)

    # Read Merchant Identity Certificate
    cert = Rails.root.join(CERTIFICATES_DIR, MERCHANT_ID_CERT_FILE).read
    http.cert = OpenSSL::X509::Certificate.new(cert)

    # Read Merchant Identity pem file
    pem = Rails.root.join(CERTIFICATES_DIR, MERCHANT_ID_PEM_FILE).read
    http.key = OpenSSL::PKey::RSA.new(pem, nil)

    request = build_request(uri)
    response = http.request(request)

    respond_to do |format|
      format.json { render json: response.body, status: :ok }
    end
  end
  
  private

  def build_http(uri)
    http = ::Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.ssl_version = :TLSv1_2
    http.ciphers = ['ECDHE-RSA-AES128-GCM-SHA256']
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    http
  end

  def build_request(uri)
    request = ::Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
    body = { merchantIdentifier: MERCHANT_ID,
             domainName: DOMAIN,
             displayName: 'ToDo Application' }
    request.body = body.to_json
    request
  end
end

1.5 Sample JSON Response

Below is a sample JSON response returned by Apple on successful authorisation of payment by user:

payment = {
  "token"=>{
    "paymentData"=>{
      "version"=>"EC_v1",
      "data"=>"VsVvw2VCOaMa11WYJthb/MpAifih9wk2fqFrrCainB...DmD+S3jo3TFgPktA64e9RtuEm1ream6BQM+5mP",
      "signature"=>"MIAGCSqGSIb3DQEHAqCAMIACAQMEAgEFA...qDDe9FD3HGlD0004rgx50gthVyLZwAAAAAAAA==",
      "header"=>{
        "ephemeralPublicKey"=>"MFkwEwYHKoZIzj0CAQYIKoj0QcDQgA...T+XzfmGFvRs/G2qwgmWY8fKu7p6Ktgxug==",
        "publicKeyHash"=>"AJiEM3d+czut7s1t4QdtRBPjSOxw0D6iWSp1MUdXueM=", 
        "transactionId"=>"f8f0c804922303decba1a8a4f7c503df1a6314e44e8db5ae7eb6b7fe0323513b"
      }
    },
    "paymentMethod"=>{
      "displayName"=>"MasterCard 1471",
      "network"=>"MasterCard",
      "type"=>"debit"
    },
    "transactionIdentifier"=>"F8F0C804922303DECBA1A8A4F7C503DF1A6314E44E8DB5AE7EB6B7FE0323513B"
  }
}

2 Setup Apple Pay on customer iPhone

For this, you will need an iPhone that supports payments through Apple Pay. Complete list of compatible devices can be found here.

2.1 Create icloud account

In test environment, you will need a sandbox tester account to make purchase using Apple Pay. A sandbox tester account can be setup by following below steps:

  1. Sign in to iTunes Connect.
  2. On the homepage, click “Users and Roles”.
  3. Click “Sandbox Testers”.
  4. Select “+” to set up your tester accounts.
  5. Complete the Tester Information form and click “Save”.

Your tester account is ready to be used.

In live environment, you can use your actual iCloud account.

2.2 Setup iPhone

  1. In Settings > Touch ID & Passcode, setup your finger print on your iPhone to make easy payments by just touching the home button.
  2. Sign in to iCloud on iPhone.
  3. In Settings > General > Language & Region, set region to one of the supported countries. Sandbox testing is supported only in these countries.
  4. Open Wallet app and:
    1. Click on “Add Credit or Debit Card”. You can scan your card here and skip steps B-D.
    2. Click on “Enter Card Details Manually”.
    3. Enter Name and Card Number and click “Next”.
      Setup wallet-10001
    4. Enter Expiration Date and Security Code and click “Next”.
      Wallet setup-10002
    5. Click “Agree”.
      Your card has been added to Wallet now. All test credit cards are available here.
      Wallet setup-10003
    6. Select the card just added, click on “i” icon at the bottom, and enter Info > Billing Address for the card.
      Billing info for the card
  5. In Settings > Wallet & Apple Pay, set Default Card, Shipping Address, Email, and Phone.
    Set default card

3 Launch application

In your iPhone, open Safari and run your application on HTTPS. You should see a working “Apple Pay” button on your payment page. Click the button and authorise the payment through touch ID.

Apple Pay button in application
After the payment is authorised by user, you can see the encrypted Apple Pay response being returned to your server for making payments on payment gateway.

In the next post, we will use the payment processing certificates to decrypt the JSON response returned by Apple. Till then, Happy Coding!

Leave a Reply

Your email address will not be published. Required fields are marked *