Broadleaf Microservices
  • v1.0.0-latest-prod

Digital Wallets (Apple Pay, Google Pay, & PayPal)

Integration Example

1. Get Available Payment Methods

At the first step we need to fetch the list of available payment methods for the transaction, based on the transaction information like amount, country, and currency. Use useAdyenGetAvailablePaymentMethods hook from the @broadleaf/adyen-payment-services-react to send this request.

import { PaymentMethodsResponse } from '@adyen/adyen-web';

import { useAdyenGetAvailablePaymentMethods } from '@broadleaf/adyen-payment-services-react';

  const {
    getAvailablePaymentMethods,
  } = useAdyenGetAvailablePaymentMethods<PaymentMethodsResponse>({...});

const paymentMethodsResponse = await getAvailablePaymentMethods({
  amount,
  shopperLocale: locale,
  countryCode: 'US',
  shopperReference: customerId,
});

2. Create AdyenCheckout

Create AdyenCheckout with the available payment methods from the first step

const adyenCheckout = await AdyenCheckout({
  environment: 'test',
  clientKey: AdyenConfig.CLIENT_KEY,
  analytics: {
    enabled: false,
  },
  locale: locale,
  countryCode: 'US',
  showPayButton: true,
  paymentMethodsResponse,
  onPaymentCompleted: handleOnPaymentCompleted,
  onPaymentFailed: handleOnPaymentFailed,
});

3. Mount Express Checkout Button

Create a common component to mount express checkout button for ApplePay, GooglePay and PayPal. Each payment type requires individual configuration. You can find configuration example in the documentation below.

import { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';

import { Core, GooglePay, ApplePay, PayPal } from '@adyen/adyen-web';

type PaymentMethodType = 'googlepay' | 'applepay' | 'paypal';

interface IContainer {
  type: PaymentMethodType;
  configuration: unknown;
  checkout: Core;
}

export const AdyenExpressCheckoutButtonContainer = ({
  type,
  configuration,
  checkout,
}: IContainer) => {
  const container = useRef(null);

  const [isPaymentTypeAvailable, setIsPaymentTypeAvailable] = useState<boolean>(
    false
  );

  useEffect(() => {
    if (!checkout || !container.current) {
      return;
    }

    if (checkout.paymentMethodsResponse?.has(type)) {
      if (type === 'googlepay' || type === 'applepay') {
        const expressCheckoutComponent =
          type === 'googlepay'
            ? new GooglePay(checkout, configuration)
            : new ApplePay(checkout, configuration);

        expressCheckoutComponent
          .isAvailable()
          .then(() => {
            expressCheckoutComponent.mount(container.current);
            setIsPaymentTypeAvailable(true);
          })
          .catch(() => {
            console.debug(type + ' is unavailable');
            setIsPaymentTypeAvailable(false);
          });
      } else if (type === 'paypal') {
        new PayPal(checkout, configuration).mount(container.current);
        setIsPaymentTypeAvailable(true);
      }
    }
  }, [checkout, setIsPaymentTypeAvailable]);

  return (
    <div
      ref={container}
      id={`adyen-${type}-express-checkout-button`}
      className={classNames('mt-1', {
        hidden: !isPaymentTypeAvailable,
      })}
    />
  );
};
Usage Example
<AdyenExpressCheckoutButtonContainer
  type="paypal"
  configuration={configuration}
  checkout={checkout}
/>

The same component can be used for the applepay and googlepay.

Configuration example

Apple Pay

When creating the payment use APPLE_PAY_EXPRESS as a payment type

const applePayConfiguration = {
  isExpress: true,
  buttonColor: 'black',
  buttonType: 'check-out',
  requiredBillingContactFields: ['name', 'postalAddress'],
  requiredShippingContactFields: [
    'postalAddress',
    'name',
    'phone',
    'email',
  ],
  onShippingContactSelected,
  onShippingMethodSelected,
  onSubmit,
  onAuthorized,
} as ApplePayConfiguration;

Google Pay

When creating the payment use GOOGLE_PAY_EXPRESS as a payment type

const googlePayConfiguration = {
  type: 'googlepay',
  isExpress: true,
  buttonType: 'checkout',
  buttonSizeMode: 'fill',
  countryCode: 'US',
  emailRequired: true,
  callbackIntents: ['SHIPPING_ADDRESS', 'SHIPPING_OPTION'],
  shippingAddressRequired: true,
  shippingAddressParameters: {
    allowedCountryCodes: ['US'],
    phoneNumberRequired: true,
  },
  shippingOptionRequired: true,
  transactionInfo: {
    countryCode: 'US',
    currencyCode: amount.currency,
    totalPriceStatus: 'ESTIMATED',
    totalPrice: toString(amount.amount),
  },
  billingAddressRequired: true,
  billingAddressParameters: {
    format: 'FULL',
  },
  paymentDataCallbacks: {
    onPaymentDataChanged,
  },
  onSubmit,
  onAuthorized,
} as GooglePayConfiguration;

PayPal

When creating the payment use PAYPAL_EXPRESS as a payment type.

Express checkout with PayPal varies a bit from the Apple Pay and Google Pay flows, in that a few of the Drop-in event handlers are called at different points of the interaction. Most notably is that the onSubmit action is called immediately once the customer clicks the PayPal button (i.e. at the very beginning of the flow). The expectation here is that an Adyen payment is created to obtain a next action object which is used to prompt the Drop-in component to show the PayPal UI. We handle this interaction via a create payment request to PaymentTransactionServices, which also creates a Broadleaf payment representation, linking Adyen’s representation to ours.

From there, the typical Drop-in events are triggered to inform us of the customer’s contact info, shipping info, etc., and we make updates to the cart along the way.

For PayPal, the final action is onAdditionalDetails indicating the customer’s intention to submit their order. At this point, we submit checkout, both validating the cart data and executing the checkout payment transaction.

Note
If the customer is anonymous you need to create the guest token before creating the payment, otherwise the payment will be archived. This is a protection mechanism for anonymous carts without valid guestToken to prevent leak of sensitive personal information.
const paypalComponentConfiguration = {
  intent: 'authorize',
  isExpress: true,
  // Note: The buyer country is only used in the sandbox.
  // Don't pass this query parameter in production.
  countryCode: 'US',
  emailRequired: true,
  blockPayPalVenmoButton: true,
  blockPayPalCreditButton: true,
  blockPayPalPayLaterButton: true,
  onSubmit: async (
    state: SubmitData,
    component: UIElement,
    actions: SubmitActions
  ) => {
    await handleSubmit(state, component, actions);
  },
  onShippingAddressChange: handleShippingAddressChange,
  onShippingOptionsChange: handleShippingOptionChange,
  onAdditionalDetails: handleAdditionalDetails,
  onAuthorized,
} as PayPalConfiguration;

Update PayPal payment

In the express checkout flow, when the PayPal button is clicked, we create a payment for a specific amount. When the shipping address/option changes, the Cart can be repriced, and the new price is reallocated to the payment. Due to this, the payment version can be changed and when you update the payment you need to use the latest payment version. In CartOperationServices, we added the CartAlert in the response to notify the caller about the updated payment. See CartOperationServices 3.0.0 Release Notes for information about this change.

Example of using this:
type PaymentSummaryStoreValueType = {
  paymentId: string;
  version: number;
};

const createPaymentSummaryStore = () => {
  let value: PaymentSummaryStoreValueType = null;
  return {
    setValue(newValue: PaymentSummaryStoreValueType) {
      value = newValue;
    },
    getValue() {
      return value;
    },
  };
};
const paymentSummaryStore = createPaymentSummaryStore();

const useHandleShippingAddressChange = ({ setErrorMsg }) => {
  const { cart } = useCartContext();
  const updateFulfillmentAddress = useUpdateFulfillmentAddress({
    buildFulfillmentAddress,
  });

  const buildShippingOptionParameters = useBuildShippingOptionParameters();

  const getPaymentAmount = useGetPaymentAmount({
    gatewayType: AdyenPaymentForm.TYPE,
  });

  const authState = usePaymentAuthState();
  const paymentClient = usePaymentClient();

  const { handleUpdatePaymentInfo } = useSubmitPaymentRequest({
    authState,
    // set "undefined" to prevent updating existing payments
    payments: undefined,
    ownerId: cart?.id,
    owningUserEmailAddress: cart?.emailAddress,
    paymentClient,
    multiplePaymentsAllowed: false,
    rejectOnError: true,
  });

  return useEventCallback(async (data, actions, component: PayPal) => {
    const { fulfillmentOptions, cart } = await updateFulfillmentAddress(
      get(data, 'shippingAddress')
    );
    const shippingOptions = buildShippingOptionParameters(
      fulfillmentOptions,
      cart
    );

    const paymentAmount = getPaymentAmount(cart);

    const { paymentId, version } = paymentSummaryStore.getValue();

    const paymentUpdateRequest = {
      paymentId,
      amount: paymentAmount,
      taxTotal: cart.cartPricing.totalTax,
      includedTaxTotal: cart.cartPricing.includedTaxAmount,
      gatewayUpdateParams: {
        paymentData: component.paymentData,
        deliveryMethods: shippingOptions,
      },
    } as UpdatePaymentRequest;

    try {
      const paymentVersionToUpdate = getLatestPaymentVersionByPaymentId(
        paymentId,
        version,
        cart.cartAlerts
      );

      const updatedPaymentSummary = await handleUpdatePaymentInfo(
        paymentUpdateRequest,
        paymentVersionToUpdate
      );

      paymentSummaryStore.setValue({
        paymentId: updatedPaymentSummary.paymentId,
        version: updatedPaymentSummary.version,
      });

      component.updatePaymentData(
        get(updatedPaymentSummary.gatewayUpdateResponse, 'paymentData')
      );
    } catch (err) {
      console.error('EXCEPTION FOR PAYMENT UPDATE:', err);
      component.setStatus('error');
      component.unmount();
    }
  }, []);
};

const getLatestPaymentVersionByPaymentId = (
  paymentId: string,
  currentVersion: number,
  alerts?: CartAlert[]
): number => {
  if (isEmpty(alerts)) {
    return currentVersion;
  }

  let latestVersion = currentVersion;

  for (const alert of alerts) {
    const updates = alert.additionalAttributes?.['UPDATED_PAYMENTS'];

    if (alert.type !== 'PRICING_CHANGE' || isEmpty(updates)) {
      continue;
    }

    const updatedPayment = find(updates, update => {
      return update.paymentId === paymentId;
    });

    if (updatedPayment?.version > latestVersion) {
      latestVersion = updatedPayment.version;
    }
  }

  return latestVersion;
};

Testing Locally

For local testing digital wallets, using ngrok is especially important. Please review the instructions provided in the environment setup document.

A few additional steps for Apple Pay:

  • In the Adyen portal, enable Apple Pay & provide your ngrok-provided URL as the shop URL. Note: Adyen currently does not support updating the shop URL once Apple Pay is enabled. Therefore, for local testing, we had to create another Adyen sub-merchant account to be able to restart the Apple Pay enabling process, & provide a new ngrok URL.

  • To complete the Apple Pay interaction in a testing context, you’ll need to create an Apple developer account, a sandbox user (i.e. a test Apple ID), & add a test card to the wallet. Instructions on how to do this can be found here.