Broadleaf Microservices
  • v1.0.0-latest-prod

ApplePay Checkout Integration Flow

Apple Pay Implementation Details in the Broadleaf Framework

It is possible to use the Apple Pay JS API or W3C Payment Request API to implement the Apple Pay payment. We used the Apple Pay JS API for our demo application and this documentation contains the examples for this API.

Providing Merchant Validation

When the user clicks on the Apple Pay button the event onvalidatemerchant is triggered to validate the merchant. The parameter of this function contains the validation URL - validationURL. This URL should be passed as the request parameter to the server - POST /api/payment/checkout-com/wallet-session/applepay/validate?validateSessionUrl={validationURL}. The response should be passed to the completeMerchantValidation method.

The example of the onvalidatemerchant event implementation:

    // The event that occurs when you click the Apple Pay button
    session.onvalidatemerchant = event => {
        const validationURL = event.validationURL;
      // "validateApplePaySession" should send the POST request to "/api/payment/checkout-com/wallet-session/applepay/validate?validateSessionUrl={validationURL}"
      validateApplePaySession(validationURL)
        .then(merchantSession => {
          session.completeMerchantValidation(merchantSession);
        })
        .catch(error => {
          console.error(error);
          session.completePayment(window.ApplePaySession.STATUS_FAILURE);
        });
    };

The endpoint com.broadleafcommerce.web.endpoint.CheckoutComWalletSessionEndpoint receives and handles this validation request.

See also Providing Merchant Validation documentation for more details.

Generate a Checkout.com token from the Apple Pay token

When the customer validates the transaction with biometrics, Apple will generate a payment token. This token should be converted into a Checkout.com card token. To do so, it has to be sent in the paymentMethodProperties of the Payment Request.

The example of the Payment Request with the Apple Pay token:

    // The event that occurs when the payment is authorized
    session.onpaymentauthorized = event => {
      const applePaymentToken = event.payment.token.paymentData;

        const paymentRequest = {
          name: 'Apple Pay',
          type: 'APPLE_PAY',
          gatewayType: 'CHECKOUT_COM',
          amount: '11.23',
          paymentMethodProperties: {
            apple_pay_token: JSON.stringify(applePaymentToken)
          }
        };

      ...
    };

On the server side before creation of the Payment object we execute the convert this token into a Checkout.com card token. The interface used to do so - com.broadleafcommerce.paymentgateway.service.PaymentGatewayExchangeWalletTokenService. By default, the exchanged token will be added to the com.broadleafcommerce.paymenttransaction.web.endpoint.domain.CreatePaymentRequest#paymentMethodProperties with the token key and the apple_pay_token will be removed. Using this converted token the standard payment request e.g. Authorize, can be executed.

See Apple Pay documentation for more details.

Set up Apple Pay

Before you get started please go to the Set up Apple Pay page and follow the steps to configure your Apple Pay integration.

After you’ve finished the setup process, you should have the following configuration:

  • A merchant ID (for example, merchant.com.mywebsite.sandbox).

  • Checkout.com linked to your merchant ID.

  • A domain verified by Apple.

  • A .key and a .pem certificate files.

Broadleaf Apple Pay Configuration

Default Certificates Provider

Our payment module includes an interface, com.broadleafcommerce.vendor.checkoutcom.service.session.CheckoutComApplePayCertificateAndKeyProvider, that is responsible for reading the ApplePay certificates. There are two implementations:

  • The com.broadleafcommerce.vendor.checkoutcom.service.session.StringBase64CheckoutComApplePayCertificateAndKeyProvider, it expects props to contain base64 encoded string representation of the certificate and key.

    • This is the default implementation since 2.0.4. You can omit the property broadleaf.checkout-com.apple-pay.certificate-and-key-provider-impl or set it to BASE64_STRING

  • The com.broadleafcommerce.vendor.checkoutcom.service.sesseion.DefaultCheckoutComApplePayCertificateAndKeyProvider, it expects props to point to certificate and key files and uses ClassPathResource to resolve them from the resources folder. This is not a recommended approach, and it is advised not to use it beyond the dev env.

    • It is deprecated as of 2.0.4, to enable it, explicitly specify the property: broadleaf.checkout-com.apple-pay.certificate-and-key-provider-impl=CLASSPATH_RESOURCE

If you want to use another certificate storage, you can implement CheckoutComApplePayCertificateAndKeyProvider and override the default bean from com.broadleafcommerce.vendor.checkoutcom.autoconfigure.CheckoutComApplePayConfiguration. For example:

    @Bean(name = "checkoutComApplePayCertificatesProvider")
    public CheckoutComApplePayCertificateAndKeyProvider checkoutComApplePayCertificatesProvider() {
        return new MyCheckoutComApplePayCertificateAndKeyProvider();
    }

Depending on your certificate format you might need to override String getCertAndKeyFormat() method to return whatever name you give to your approach. If you’ve overridden String getCertAndKeyFormat() you will need to override DefaultCheckoutComApplePayWebClientProvider#getSslContext to put a matching code for your new approach. Specifically around this part:

            if (DefaultApplePayCertificateAndKeyFormats.DER
                    .equals(certificatesProvider.getCertAndKeyFormat())) {
                privateKey = readPrivateKeyFromString(keyInputStream);
                clientCertificate = loadCertificateFromString(certificateInputStream);
            } else if (DefaultApplePayCertificateAndKeyFormats.FULL_PEM_FILE
                    .equals(certificatesProvider.getCertAndKeyFormat())) {
                privateKey = readPrivateKey(keyInputStream);
                clientCertificate = loadCertificate(certificateInputStream);
            } else {
                throw new CheckoutComApplePayCertificateException(String.format(
                        "The provided certificate format %s is not supported. Supported formats are: DER and FULL_PEM_FILE",
                        certificatesProvider.getCertAndKeyFormat()));
            }

The full method looks like this:

    protected SslContext getSslContext(@Nullable String applicationId, @Nullable String tenantId) {
        final char[] keyPassword = RandomStringUtils.randomAlphabetic(16).toCharArray();

        try (InputStream keyInputStream =
                certificatesProvider.getPrivateKeyInputStream(applicationId, tenantId);
                InputStream certificateInputStream =
                        certificatesProvider.getCertificateInputStream(applicationId, tenantId);) {
            PrivateKey privateKey;
            Certificate clientCertificate;
            if (DefaultApplePayCertificateAndKeyFormats.DER
                    .equals(certificatesProvider.getCertAndKeyFormat())) {
                privateKey = readPrivateKeyFromString(keyInputStream);
                clientCertificate = loadCertificateFromString(certificateInputStream);
            } else if (DefaultApplePayCertificateAndKeyFormats.FULL_PEM_FILE
                    .equals(certificatesProvider.getCertAndKeyFormat())) {
                privateKey = readPrivateKey(keyInputStream);
                clientCertificate = loadCertificate(certificateInputStream);
            } else {
                throw new CheckoutComApplePayCertificateException(String.format(
                        "The provided certificate format %s is not supported. Supported formats are: DER and FULL_PEM_FILE",
                        certificatesProvider.getCertAndKeyFormat()));
            }

            KeyStore keyStore = KeyStore.getInstance("jks");
            keyStore.load(null, null);

            String certificateAlias =
                    getUniqueApplePayClientCertificateAlias(applicationId, tenantId);
            String keyAlias = getUniqueApplePayClientKeyAlias(applicationId, tenantId);

            keyStore.setCertificateEntry(certificateAlias, clientCertificate);
            keyStore.setKeyEntry(keyAlias, privateKey, keyPassword,
                    new Certificate[] {clientCertificate});

            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
            keyManagerFactory.init(keyStore, keyPassword);

            return SslContextBuilder.forClient()
                    .keyManager(keyManagerFactory)
                    .build();

        } catch (Exception e) {
            throw new CheckoutComApplePayCertificateException(
                    "An error occurred while creating the Apple Pay SSL context with the provided certificates.",
                    e);
        }
    }

Add Certificates

Since 2.0.4 the recommended approach is to provide a certificate as base64 encoded string rather than path to a file

Add Certificates via `StringBase64CheckoutComApplePayCertificateAndKeyProvider`(since 2.0.4)

Since 2.0.4 it is advised to use new default com.broadleafcommerce.vendor.checkoutcom.service.sesseion.StringBase64CheckoutComApplePayCertificateAndKeyProvider.

Let’s assume that your certificate file names are private_key_sandbox.key and certificate_sandbox.pem. Using OpenSSL or similar, extract the certificate_sandbox.pem and private_key_sandbox.key files, take the base64 encoded value within those PEM files (ignoring the -----BEGIN header & -----END footer, and removing all the line breaks), and add those encoded strings to your broadleaf.checkout-com.apple-pay.certificate-string and broadleaf.checkout-com.apple-pay.private-key-string properties. Alternatively, if your certificate and key files are in the binary DER format, you can just base64 encode the entire files themselves and supply the output as the property values. Then, provide those strings securely via configuration properties. If you are using the ConfigServer, these can go under the secure properties there, otherwise you can provide them via environment variables in your deployment configuration.

Add Certificate Provider Configuration Properties:

broadleaf:
  checkout-com:
    apple-pay:
      merchant-identifier: merchant.com.mydomain.test (1)
      display-name: MyCommerce (2)
      initiative-context: mydomain.com (3)
      certificate-string: (base64-encoded representation of entire certificate.pem file without the first and last lines and linebreaks) (4)
      private-key-string: (base64-encoded representation of entire privatekey.pem file without the first and last lines and linebreaks) (5)
  1. The merchant ID configured in your Apple Pay Developer account

  2. A string of 64 or fewer UTF-8 characters containing the canonical name for your store, suitable for display

  3. This should be equal to the domain you verified in your Apple Pay Developer account

  4. The string in base64 format representing the Apple certificate ONLY WHEN YOU USE StringBase64CheckoutComApplePayCertificateAndKeyProvider

  5. The string in base64 format representing the Apple certificate private key ONLY WHEN YOU USE StringBase64CheckoutComApplePayCertificateAndKeyProvider

(deprecated) Add Certificates via `DefaultCheckoutComApplePayCertificateAndKeyProvider`(deprecated since 2.0.4)

For the not recommended deprecated(since 2.0.4) com.broadleafcommerce.vendor.checkoutcom.service.sesseion.DefaultCheckoutComApplePayCertificateAndKeyProvider you can do the following: Let’s assume that your certificate file names are private_key_sandbox.key and certificate_sandbox.pem.

Put these certificate files into the resources folder depending on your Flex Package deployment type. You can use the inner folder e.g resources/applepay to separate these files from others. For example:

  • Min - ../flexpackages/min/src/main/resources/applepay

  • Balanced - ../flexpackages/balanced/cart/src/main/resources/applepay

  • Granular - You have to put the files to the resources of Payment Transaction Service

Add Certificate Provider Configuration Properties:

broadleaf:
  checkout-com:
    apple-pay:
      merchant-identifier: merchant.com.mydomain.test (1)
      display-name: MyCommerce (2)
      initiative-context: mydomain.com (3)
      certificate: applepay/certificate_sandbox.pem (4)
      private-key: applepay/private_key_sandbox.key (5)
      certificate-and-key-provider-impl: CLASSPATH_RESOURCE (6)
  1. The merchant ID configured in your Apple Pay Developer account

  2. A string of 64 or fewer UTF-8 characters containing the canonical name for your store, suitable for display

  3. This should be equal to the domain you verified in your Apple Pay Developer account

  4. The path to the Apple certificate ONLY WHEN YOU USE DefaultCheckoutComApplePayCertificateAndKeyProvider

  5. The path to the Apple certificate private key ONLY WHEN YOU USE DefaultCheckoutComApplePayCertificateAndKeyProvider

  6. Since 2.0.4 it is deprecated, and you need to explicitly specify the implementation of the provider.

Add CartOps Configuration Properties:

broadleaf:
  cartoperation:
    service:
        checkout:
          checkout-payment-method-options:
            - payment-method-type: CREDIT_CARD
              payment-method-gateway-type: CHECKOUT_COM
            - payment-method-type: APPLE_PAY
              payment-method-gateway-type: CHECKOUT_COM

Apple Pay Domain Names for Merchant Validation

There is a list of available domain names to validate the merchant session object, see Setting Up Your Server.

By default, we allow the following domains:

  • apple-pay-gateway-cert.apple.com

  • apple-pay-gateway.apple.com

  • apple-pay-gateway-nc-pod1.apple.com

  • apple-pay-gateway-nc-pod2.apple.com

  • apple-pay-gateway-nc-pod3.apple.com

  • apple-pay-gateway-nc-pod4.apple.com

  • apple-pay-gateway-nc-pod5.apple.com

  • apple-pay-gateway-pr-pod1.apple.com

  • apple-pay-gateway-pr-pod2.apple.com

  • apple-pay-gateway-pr-pod3.apple.com

  • apple-pay-gateway-pr-pod4.apple.com

  • apple-pay-gateway-pr-pod5.apple.com

If you need to change this list use broadleaf.checkout-com.apple-pay.valid-merchant-validation-domains property.

For more information see Providing Merchant Validation

Apple Pay JS API Integration Example

In the following example we used the Apple Pay JS API. It is also possible to use the W3C Payment Request API.

Please note that this is just an example and doesn’t contain the fully functional code.

export const CheckoutComApplePayButton = () => {
  const applePaySession = window.ApplePaySession;

  const [canPayWithApplePay, setCanPayWithApplePay] = useState(false);

  useEffect(() => {
      // Check that the payment can be made with Apple Pay
    if (applePaySession && applePaySession.canMakePayments()) {
      setCanPayWithApplePay(true);
    } else {
      setCanPayWithApplePay(false);
    }
  }, [applePaySession]);

  const handleClick = useHandleClick();

  if (!canPayWithApplePay) {
    return null;
  }

  return (
    <div>
      <div
        className="apple-pay-button apple-pay-button-black"
        lang='en'
        onClick={handleClick}
      ></div>
      {error && (
        <strong className="block my-4 text-red-600 text-lg font-normal">
          {error}
        </strong>
      )}
    </div>
  );
};

const useHandleClick = () => {
    const { createPayment } = useCreatePayment();
  return () => {
    // Create the Apple Pay session
    // see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession
    const session = new window.ApplePaySession(6, {
      countryCode: 'US',
      currencyCode: 'USD',
      supportedNetworks: ['visa', 'masterCard'],
      merchantCapabilities: ['supports3DS'],
      total: {
        label: 'My Store',
        amount: '11.23'
      }
    });

    // The event that occurs when you click the Apple Pay button
    session.onvalidatemerchant = event => {
        const validationURL = event.validationURL;
      // "validateApplePaySession" should send the POST request to "/api/payment/checkout-com/wallet-session/applepay/validate?validateSessionUrl={validationURL}"
      validateApplePaySession(validationURL)
        .then(merchantSession => {
          session.completeMerchantValidation(merchantSession);
        })
        .catch(error => {
          console.error(error);
          session.completePayment(window.ApplePaySession.STATUS_FAILURE);
        });
    };

    // The event that occurs when the payment is authorized
    session.onpaymentauthorized = event => {
      const applePaymentToken = event.payment.token.paymentData;

      createPayment(JSON.stringify(applePaymentToken), session);
    };

    session.begin();
  };
};
const useCreatePayment = () => {

  const createPayment = async (
    applePaymentToken,
    appleSession
  ) => {
    const paymentRequest = {
      name: 'Apple Pay',
      type: 'APPLE_PAY',
      gatewayType: 'CHECKOUT_COM',
      amount: '11.23',
      paymentMethodProperties: {
        apple_pay_token: applePaymentToken
      }
    };

    let paymentSummary;

    try {
      paymentSummary = await handleSubmitPaymentInfo(paymentRequest);
      if (paymentSummary) {
        appleSession.completePayment(window.ApplePaySession.STATUS_SUCCESS);
      }
    } catch (err) {
      console.error('There was an error adding payment information', err);

      appleSession.completePayment(window.ApplePaySession.STATUS_FAILURE);
    }
  };

  return { createPayment };
};

Testing Locally

In this example we use the Ngrok to have the ability to open the storefront started locally from the Apple device. Please install and configure it before testing the Apple Pay integration.

To test the Apple Pay integration follow the next steps:

  1. Go to Testing Apple Pay page and configure your Apple Pay device.

  2. Configure your environment as described in this documentation above. Do not validate any domains in the Apple Developer, you will need to do it later. Please note that the property broadleaf.checkout-com.apple-pay.initiative-context will need to be changed later with the domain generated by ngrok, for example, c746-176-115-97-170.eu.ngrok.io without https:// (we will use this URL for this documentation).

  3. Add the following configuration:

     broadleaf:
       tenant:
         urlresolver:
           application:
             port: '443'
             domain: 'eu.ngrok.io'
  4. Run your app locally as usual

  5. Execute ngrok http https://{yourAppUri}/ to build the tunnel to your local app. Replace yourAppUri with your app URI e.g myapp.localhost:8456

  6. Change the broadleaf.checkout-com.apple-pay.initiative-context property with the generated domain - c746-176-115-97-170.eu.ngrok.io and restart your server.

  7. In the Admin panel go to the Security → Authorization Servers, open your app configuration and click on Authorized Clients tab (/authorization-servers/{yourAppId}?form=authorizedClients). Add the Redirect Urls - https://c746-176-115-97-170.eu.ngrok.io/callback and https://c746-176-115-97-170.eu.ngrok.io/silent-callback.html to your Authorized Client.

  8. In the Admin panel go to the Tenant Management → Applications and open your application configuration from the list. In the Application Identifier field replace the value with eu.ngrok.io. This step is needed to resolve your application. See com.broadleafcommerce.tenant.web.endpoint.TenantResolverEndpoint.

  9. Go to the Apple Developer and validate the domain - c746-176-115-97-170.eu.ngrok.io. See Validate your domain. During the validation the text file will be generated, and it should be put to the .well-known folder in the root of your server. If you are using React to build your app, you can put it to the public folder in the root of your project.

If you made everything correctly, you should be able to open your app by https://c746-176-115-97-170.eu.ngrok.io URL on your Apple Device.

Troubleshooting

If your page is blank please make sure you configured your content security policy with Content-Security-Policy correctly or disable it temporary. It can restrict the domain generated by ngrok.

If the Apple Pay button is displayed but when you click on it and see Processing for a long period of time, make sure the request to validate the session is executed without any errors, see com.broadleafcommerce.vendor.checkoutcom.service.sesseion.CheckoutComWalletSessionService.validateApplePaySession.

If the session validation looks good but the payment failed, make sure you specified broadleaf.checkout-com.apple-pay.initiative-context with the domain you verified on the Apple Developer. Make sure the value doesn’t contain https://, for example instead of https://c746-176-115-97-170.eu.ngrok.io use c746-176-115-97-170.eu.ngrok.io.

If the request to create the Payment failed, make sure you are sending apple_pay_token: applePaymentToken in the paymentMethodProperties. Check if this token is exchanged without any errors, see com.broadleafcommerce.vendor.checkoutcom.service.token.ExchangeWalletTokenService.exchangeWalletToken.