Overview

Stripe.js V3 libraries allow you to accept payments from shoppers through 25+ different payment methods with a single, embeddable UI component. This includes Apple and Google Pay for supported devices and browsers. Stripe powers this through their Payment Element, which requires both front-end and server-side work in order to accept a payment.

Since Apple Pay stores customer information, such as Shipping and Billing Information, integrating with these payment methods simplifies checkout for shoppers since they no longer need to re-enter this information during checkout. Violet checkout has been enhanced to enable you to use Apple Pay through Stripe Payment Elements to create easier, more secure payment flows for your shoppers. This includes the following flows for Shoppers:

  • Selecting contact information through Apple Pay
  • Selecting a shipping address through Apple Pay
  • Selecting a payment method through Apple Pay
  • Selecting shipping methods through Apple Pay
  • Confirming the payment through FaceId/TouchId

This document walks you through how you can integrate with these changes. For more information on adding Apple Pay into your system, please refer to the Stripe Guides, available here.

For a detailed, end-to-end example of how Violet Checkout works with Apple Pay, you can download our sample application: Violet Sample App.

We highly recommend referencing this code base during your implementation as it can help answer many questions and guide you in the right direction.

This document assumes that you have already set up Violet within your software ecosystem and integrated with our APIs. If this is not the case, please follow and read through Quick Start guide.

Creating an Order

The first step is to create a cart with items in it for your shopper as described here, however, with the following new flag added to the request body:

{
  "wallet_based_checkout": true
}

This flag is required to ensure the rest of the checkout flow is aware that an external wallet will be used to complete the payment.

Carts with wallet_based_checkout set to true cannot be completed without completing the front end client work listed below, specifically the Stripe Element integration and the client side Payment Confirmation. There is no API-only path complete such an order and attempting to do so will return errors.

This will initialize the cart as a wallet based cart allowing you to utilize a payment intent rather than raw credit card details. When wallet_based_checkout is true the response will include two new properties:

{
  "stripe_key": "pk_test_UHg8oLvg4rrDCbvtqfwTE8qd",
  "payment_intent_client_secret": "pi_3MbFHUK29KDiBVld0N8b8EDd_secret_cb5AOMAxXyZeJfy4wylgnVd3C"
}

You will need both values to render the Apple Payment Sheet in the steps below.

The stripe_key only changes at the environment level and is used to load the Stripe libraries. We recommend saving it as an environment variable in your client-side code so that you don’t need to re-render Stripe elements each time.

The payment_intent_client_secret changes per order — this is what is used to tie the order to the payment. It does not change during the order though. Violet automatically updates this payment intent with correct totals as you make changes to the cart (adding/removing skus, shipping, discounts).

Rendering the Apple Payment Sheet

Once a cart has been created, the rest of the checkout flow can take place on the Apple Payment Sheet. This includes:

  • Applying customer information
  • Selecting shipping addresses
  • Selecting shipping methods
  • Selecting payment information
  • Confirming the order.

Apple also allows you to only request parts of the information from the list above if you already have details about the customer. Hence, there are multiple possible ways checkout can be completed through Apple Pay. The two most common ways are:

  • Apple Pay Payment — In this scenario, customer information and shipping and billing addresses are requested prior to displaying the Apple Pay term sheet so that only Payment takes place through Apple Pay.
  • Apple Pay Checkout — In this scenario, all information is requested and attached to the cart through Apple Pay.

Apple Pay Payment

To support the “Payment-only using Apple Pay” flow, where only the charge is completed through Apple Pay, please follow the integration guide available here: Violet Checkout with Stripe.js v3. Since Stripe Payment Elements have multiple payment methods built into it, Apple Pay being one of them, this solution will work out of the box to complete payments.

Apple Pay Checkout

Retrieving the Payment Intent Client Secret

To render the Apple Payment Sheet, Violet recommends using Stripe elements. This ensures that the payment can successfully be handled by Violet without requiring any extra effort on your side.

In order to render a Stripe Payment Element, Stripe requires both a Stripe Key as well as a Payment Intent Client Secret. Since Violet handles the payment flow for you, you will have both values from the original create cart response from above.

Integrating with the Stripe Payment Request

The Payment Request Button Element gives you a single integration for Apple Pay, Google Pay, and Link — Stripe’s one-click checkout product. Now that you have a Stripe Key and Payment Intent Client Secret, you’re ready to render the Stripe Payment Request button and collect all shopper information through Apple Pay.

Set up Stripe.js

Install the Stripe.js React libraries and the Stripe JS loader from the npm public registry:

npm install --save @stripe/react-stripe-js @stripe/stripe-js

Add Stripe.js and Elements to your payment page

To use the Stripe Payment element, wrap your checkout page in a Stripe Elements provider. This is where you will also use the Stripe Key provided in the Order response.

import React from 'react';
import ReactDOM from 'react-dom';
import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';

import CheckoutForm from './CheckoutForm';

// Make sure to call `loadStripe` outside of a component's render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe('pk_test_UHg8oLvg4rrDCbvtqfwTE8qd');

function App() {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm />
    </Elements>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

Add the Payment Request to your Checkout Form

In your checkout form, render the Stripe PaymentElement component:

import React, {useState, useEffect} from 'react';
import {PaymentRequestButtonElement, useStripe} from '@stripe/react-stripe-js';

const CheckoutForm = () => {
  const stripe = useStripe();
  const [paymentRequest, setPaymentRequest] = useState(null);

  useEffect(() => {
    if (stripe) {
      const pr = stripe.paymentRequest({
        country: 'US', //From the Order object
        currency: 'usd', //From the the Order object
        total: {
          label: 'Sub Total', //From the Order object
          amount: 1099,
        },
        requestPayerName: true,
        requestPayerEmail: true,
				requestShipping: true
      });

			// Check the availability of the Payment Request API.
      pr.canMakePayment().then(result => {
        if (result) {
          setPaymentRequest(pr);
        }
      });

    }
  }, [stripe]);

	if (paymentRequest) {
    return <PaymentRequestButtonElement options={{paymentRequest}} />
  }

  // Use a traditional checkout form.
  return 'Insert your form or button component here.';
}

export default CheckoutForm;

Use the requestPayerName parameter to collect the payer’s billing address for Apple Pay. You can use the billing address to perform address verification and block fraudulent payments. All other payment methods automatically collect the billing address when one is available.

Next, learn how to apply customer information and request shipping methods.

Applying Customer Information and Requesting Shipping Methods

Listen to the shippingaddresschange event to detect when a shipping address is selected by the shopper. Use this address to fetch valid shipping options from Violet.

Applying a Shipping Address to an order is required to retrieve Shipping Methods and present them to the shopper.

However, Apple does not return a complete shipping address to your code, omitting information like the actual street address for privacy reasons. Violet accounts for this by allowing wallet_based_checkout carts to pass an empty string for address_1 at this point in the checkout flow.

Once the payment is confirmed, Apple will provide the address info. That info must be sent in the /submit request body as shown below so the real address can be finalized during order submit.

// Handle `shippingaddresschange` event
paymentRequest.on('shippingaddresschange', function(event) {
  var updateWith = event.updateWith;
  var shippingAddress = event.shippingAddress;

  // handle shippingaddresschange event
  // Call Violet API to apply customer info to Cart
  const orderResponse = await handleCustomerInfo();

  // Call Violet API to get shipping methods for selected address
  const shippingMethods = await handleFetchShipping();

  // Apply shipping methods to updateDetails object

  // call event.updateWith within 30 seconds
  updateWith(updateDetails);
});

// Handler 'event' object
{
	firstName: "",
	lastName: "",
	email: "",
  shippingAddress: {
    country: "US",
    addressLine: [
      "185 Berry St.",
      "Suite 500",
    ],
    region: "CA",
    city: "San Francisco",
    ...,
  },
  updateWith: function(updateDetails) {
    // Call this function to merge your updates into the current
    // PaymentRequest object. Note that you must call this
    // within 30 seconds.
  },
}

const handleFetchShipping = async (result) => {
  //Call to Violet to get available shipping methods
  const res = await fetch('{{baseUrl}}/checkout/cart/:cart_id/shipping', {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' }
  });

  const shipping = await res.json();

}

const handleCustomerAddress = async (result) => {
  //Call to Violet to apply the customer's address
  const res = await fetch('{{baseUrl}}/checkout/cart/:cart_id/customer', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
			body: {
                "first_name": "Ishan",
                "last_name": "Wallet Checkout Tests",
                "email": "ishan.guru+test_order@violet.io",
                "shipping_address": {
                    "address_1":"", //Apple will not provide this to you yet
                    "city":"Brooklyn",
                    "country":"US",
                    "postal_code":"11206",
                    "state":"NY",
                    "type":"SHIPPING"
                },
                "billing_address": {
                    "address_1":"", //Apple will not provide this to you yet
                    "city":"Seattle",
                    "country":"US",
                    "postal_code":"98109",
                    "state":"WA",
                    "type":"BILLING"
                }
			}
    });

  const orderResponse = await res.json();
}

Applying the Shipping Method

Once shipping methods have been shown to the Shopper, you can subscribe to the shippingoptionchange event to select and apply new shipping methods to the cart.

// Handle `shippingaddresschange` event
paymentRequest.on('shippingoptionchange', function(event) {
  var updateWith = event.updateWith;
  var shippingOption = event.shippingOption;
  // Send shipping method selected to Violet API
  handleShippingChange();

  // call event.updateWith within 30 seconds
  updateWith(updateDetails);
});

// Handler event object
{
  shippingOption: {
    id: "someUniqueID",
    label: "Ground",
    detail: "UPS standard ground shipping",
    amount: 999,
  },
  updateWith: function(updateDetails) {
    // Call this function to merge your updates into the current
    // PaymentRequest object. Note that you must must call this
    // within 30 seconds.
  },
}

// Call to update Violet Shipping
const handleShippingChange = async (result) => {

	//Call to Violet to apply shipping method
  const res = await fetch('{{baseUrl}}/checkout/cart/:cart_id/shipping?price_cart=true', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify([
		    {{shippingData}}
			])
    });

  const orderResponse = await res.json();

}

Confirming the Payment

Once you’ve applied shipping methods to all the bags in the order, you can confirm the payment using a paymentMethodId and paymentIntentClientSecret.

Listen to the paymentmethod event to receive a PaymentMethod object and then confirm the card payment.

paymentRequest.on('paymentmethod', async (ev) => {
  // Confirm the PaymentIntent without handling potential next actions (yet).
  const {paymentIntent, error: confirmError} = await stripe.confirmCardPayment(
    clientSecret,
    {payment_method: ev.paymentMethod.id},
    {handleActions: false}
  );

  if (confirmError) {
    // Report to the browser that the payment failed, prompting it to
    // re-show the payment interface, or show an error message and close
    // the payment interface.
    ev.complete('fail');
  } else {
    // Report to the browser that the confirmation was successful, prompting
    // it to close the browser payment method collection interface.
    ev.complete('success');
    // Check if the PaymentIntent requires any actions and if so let Stripe.js
    // handle the flow. If using an API version older than "2019-02-11"
    // instead check for: `paymentIntent.status === "requires_source_action"`.
    if (paymentIntent.status === "requires_action") {
      // Let Stripe.js handle the rest of the payment flow.
      const {error} = await stripe.confirmCardPayment(clientSecret);
      if (error) {
        // The payment failed -- ask your customer for a new payment method.
      } else {
        handleVioletSubmit(ev)
      }
    } else {
			handleVioletSubmit(ev)
    }
  }
});

Submitting the Order to Violet

If a successful result is returned from the call to Stripe, the payment has been authorized. You can now submit the Order to Violet.

Applying Final Customer Information

After post-payment confirmation, Apple stops redacting customer information such as street address. It will now be available so you will need to re-apply this information to the cart. This can be done during the submit call itself.

You can apply the customer information in the handleVioletSubmit method called above.

const handleVioletSubmit = async (result) => {
  const res = await fetch('{{baseUrl}}/checkout/cart/:cart_id/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: {
			  "order_customer" :{
			        "first_name": "P.",
			        "last_name": "Sherman",
			        "email": "p.sherman@findingnemo.com",
			        "shipping_address": {
			            "address_1":"42 Wallaby Way",
			            "city":"Sydney",
			            "country":"AU",
			            "postal_code":"4000",
			            "state":"NSW",
			            "type":"SHIPPING"
			        },
			        "same_address": true
			    }
			}
    });

  const orderResponse = await res.json();

  handleOrderConfirmation(orderResponse);
}

The order has now been submitted. You can display a confirmation screen to your Shopper upon success and close out from the Apple Pay Payment Sheet.

Limitations

Multi-merchant Checkout

Multi-merchant checkout with Apple Pay requires additional work due to the nuances around shipping methods for these orders. The Apple Pay Term Sheet was not built to allow for selecting different shipping methods within the same order. This means that to implement Violet’s support for multi-merchant checkout with different shipping methods for different merchants within the same order, we recommend using the Apple Pay Payment flow instead of the Apple Pay Checkout flow. This will let you collect user information and allow them to select the shipping methods for each bag before paying with Apple Payment.

The best approach is a hybrid method where single merchant checkout allows for the full Apple Pay experience and then multi bag checkout only allows for Apple Payment. See the Violet Sample App for a working example of this that you can reference.

Alternative

If your app still requires the Apple Pay Checkout experience, Violet suggests the following approach:

Note: This approach results in a sub-optimal user experience and increases your monthly payments to Violet. We highly recommend using the Apple Pay Payment flow instead.

If you look closely at the Order object for multi-merchant Orders, you will notice that each Bag contains details, including tax, shipping, and totals; specific to a single merchant.

{
    "id": 747,
    "token": "0fb48193744a4fe08e18f586830e8e04",
    "user_id": 10238,
    "app_id": 10193,
    "developer_id": 10122,
    "customer": { ... },
    "bags": [
        {
            "id": 1091,
            "order_id": 747,
            "merchant_id": 10131,
            "app_id": 10193,
            "external_id": "445",
            "status": "ACCEPTED",
            "fulfillment_status": "PROCESSING",
            "financial_status": "PAID",
            "skus": [ ... ],
            "shipping_method": {
                "type": "FLAT_RATE_PRICE",
                "carrier": "OTHER",
                "label": "Flat Rate",
                "price": 500,
                "id": 1368,
                "shipping_method_id": "d8fb74f7cd682de8ae35a7fdd1e26f56",
                "bag_id": 1091,
                "merchant_id": 10131
            },
            "taxes": [
                {
                    "order_id": 747,
                    "merchant_id": 10131,
                    "state": "NY",
                    "amount": 2305,
                    "description": "Tax"
                }
            ],
            "sub_total": 23045,
            "shipping_total": 500,
            "tax_total": 2305,
            "total": 25850,
            "taxes_included": false,
            "transactions": [ ... ],
            "external_checkout": true,
            "commission_rate": 0.0,
            "date_created": "2023-03-29T18:12:55+0000",
            "remorse_period_ends": "2023-04-28T18:12:55+0000",
            "currency": "USD",
            "external_currency": "USD",
            "channel": "APP",
            "platform": "BIGCOMMERCE",
            "fulfillments": [],
            "wallet_based_checkout": true,
            "bag_id": 1091,
            "bag_status": "ACCEPTED",
            "merchant_name": "Violet BC"
        },
				{
            "id": 1092,
            "order_id": 747,
            "merchant_id": 10132,
            "app_id": 10193,
            "external_id": "445",
            "status": "ACCEPTED",
            "fulfillment_status": "PROCESSING",
            "financial_status": "PAID",
            "skus": [ ... ],
            "shipping_method": {
                "type": "HEAVY_DUTY_SHIPPING",
                "carrier": "OTHER",
                "label": "Heavy Duty",
                "price": 1000,
                "id": 1368,
                "shipping_method_id": "d8fb74f7cd682de8ae35a7fdd1e26f56",
                "bag_id": 1092,
                "merchant_id": 10132
            },
            "taxes": [
                {
                    "order_id": 747,
                    "merchant_id": 10132,
                    "state": "NY",
                    "amount": 2605,
                    "description": "Tax"
                }
            ],
            "sub_total": 23045,
            "shipping_total": 1000,
            "tax_total": 2605,
            "total": 26650,
            "taxes_included": false,
            "transactions": [ ... ],
            "external_checkout": true,
            "commission_rate": 0.0,
            "date_created": "2023-03-29T18:12:55+0000",
            "remorse_period_ends": "2023-04-28T18:12:55+0000",
            "currency": "USD",
            "external_currency": "USD",
            "channel": "APP",
            "platform": "SHOPIFY",
            "fulfillments": [],
            "wallet_based_checkout": true,
            "bag_id": 1091,
            "bag_status": "ACCEPTED",
            "merchant_name": "Violet Shopify"
        }
    ],
    "shipping_address": { ... },
    "billing_address": { ... },
    ...
}

In order to support the Apple Pay term sheet, you will need to split the order from 1 Order with N bags, to N orders each with 1 bag. Each merchant will then have its own order. Prior to the rendering the Apple Pay screens, you can create these new Orders against Violet with SKU and any other information you already have pre-filled, and then guide your shopper through multiple Apple Pay payment flows. This will complete each order individually, resulting in multiple charges to the shopper on their payment methods, increased payment processing fees for you, and increased payments to Violet depending on your pricing model.