From b1713414c1a595df83bebaa0d1d66ec6852a66f1 Mon Sep 17 00:00:00 2001 From: Michel Date: Mon, 27 Jan 2025 16:22:56 +0100 Subject: [PATCH] feat(docs): add combined docs for paypal (#1641) --- apps/docs/.vitepress/sidebar.ts | 4 +- .../docs/src/.assets/payment-icons/paypal.png | Bin 0 -> 7023 bytes .../resources/integrations/payments/index.md | 6 +- .../integrations/payments/paypal-express.md | 200 ------ .../resources/integrations/payments/paypal.md | 651 ++++++++++++++++++ examples/express-checkout/README.md | 2 + 6 files changed, 658 insertions(+), 205 deletions(-) create mode 100644 apps/docs/src/.assets/payment-icons/paypal.png delete mode 100644 apps/docs/src/resources/integrations/payments/paypal-express.md create mode 100644 apps/docs/src/resources/integrations/payments/paypal.md diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts index 42c80de46..8c47b3802 100644 --- a/apps/docs/.vitepress/sidebar.ts +++ b/apps/docs/.vitepress/sidebar.ts @@ -322,8 +322,8 @@ export const sidebar = [ link: "/resources/integrations/payments/mollie.html", }, { - text: "Paypal Express", - link: "/resources/integrations/payments/paypal-express.html", + text: "Paypal", + link: "/resources/integrations/payments/paypal.html", }, ], }, diff --git a/apps/docs/src/.assets/payment-icons/paypal.png b/apps/docs/src/.assets/payment-icons/paypal.png new file mode 100644 index 0000000000000000000000000000000000000000..47f8f327fe940e7df4c2c9be13513e0b6496d0d4 GIT binary patch literal 7023 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 literal 0 HcmV?d00001 diff --git a/apps/docs/src/resources/integrations/payments/index.md b/apps/docs/src/resources/integrations/payments/index.md index db59ac0b8..cc8f9414d 100644 --- a/apps/docs/src/resources/integrations/payments/index.md +++ b/apps/docs/src/resources/integrations/payments/index.md @@ -25,9 +25,9 @@ nav:
diff --git a/apps/docs/src/resources/integrations/payments/paypal-express.md b/apps/docs/src/resources/integrations/payments/paypal-express.md deleted file mode 100644 index acdd0c1a2..000000000 --- a/apps/docs/src/resources/integrations/payments/paypal-express.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -head: - - - meta - - name: og:title - content: "PayPal Express Integration" - - - meta - - name: og:description - content: "In this chapter you will learn how to implement a custom payment flow based on PayPal Express Checkout." - - - meta - - name: og:image - content: "https://frontends-og-image.vercel.app/Paypal%20Express%20Checkout.png" ---- - - - -# PayPal Express Integration - -Paypal Express Logo - -:::tip Advanced Guide - prior knowledge required -In order to follow this guide properly, we recommend that you get familiar with the payment flow and payment API concepts first. - -- [Payment Flow in Shopware 6](https://developer.shopware.com/docs/concepts/commerce/checkout-concept/payments) -- [Payment API](https://shopware.stoplight.io/docs/store-api/8218801e50fe5-handling-the-payment) - ::: - -In this chapter you will learn how to integrate a payment flow with Shopware Frontends. There are various ways in which payment providers integrate with Shopware's API, so it is likely that you need to consult the documentation of your payment provider to get the details. - -This specific guides shows how to integrate the **PayPal Express Checkout**. However, the general flow is the same for all payment providers, so you will be able to use this guide as a reference for different providers. - -Specifically, you will learn how to - -- Prepare the Shopware instance for taking PayPal payments -- Embed payment buttons in your frontend -- React on PayPal events to prepare and capture the payment - -## Install the payment extension - -Payment integrations require communication with the backend for various scenarios - -- Create a PayPal order -- Inform PayPal which payment was selected -- Capture the user payment after approval from the provider -- Update the order status -- Notify customers on successful/failed payment -- Other actions that need additional credentials which should stay hidden (i.e. secret authorization tokens) - -That's why the backend as a Payment middleware is a good option to store additional information, credentials, react on events and so on. - -:::tip -Make sure that the Payment Provider you would like to install, provides also an interface to interact via Store-API for headless solutions, specially when it's a synchronous payment flow. -::: - -The [SwagPayPal](https://github.com/shopwareLabs/SwagPayPal) extension is available on Shopware Cloud stores and also can be installed manually in self-managed instances. It provides useful endpoints to conduct payments with PayPal. We will be using two PayPal-specific endpoints in this guide: - -### Create order - -`/store-api/paypal/express/create-order` - -- Creates an order directly with PayPal which contains information about the cart and the user -- Returns a payment intent token that identifies the order in PayPal - -### Prepare checkout - -`/store-api/paypal/express/prepare-checkout` - -- Used after PayPal approved the payment process request -- Registers a customer based on PayPal account's data (name, address, email) and logs them in -- The API Client receives a new context token that points to the logged-in customer - -## Embed Express Payment buttons - -The next step is to embed the PayPal Express Checkout buttons in your frontend using the PayPal Javascript SDK. The SDK can be loaded from the PayPal CDN or using an npm package ([PayPal SDK Documentation](https://developer.paypal.com/sdk/js/configuration/)). In our example we're going to use the second option. - -### Load the PayPal SDK - -:::tip Client only -The PayPal SDK and all its methods should only be invoked on client side rendered pages. -::: - -In a Vue component we can use the `loadScript` method from the [`@paypal/paypal-js`](https://www.npmjs.com/package/@paypal/paypal-js) npm package: - -```ts -import { loadScript } from "@paypal/paypal-js"; - -loadScript({ - // client id is generated in the PayPal account's apps section - "client-id": - "AUAcLFoadrmy9JiW2cHgriy1mTy0MCqQOP_1SSeQEUArz_zPeF1VcNY2CCxcFBQpf_N4g1k5wFVNJ1Bk", - currency: "EUR", // or use some reference to the current currency - locale: "en_US", // as same as in the field above -}); -``` - -Now, the `paypal` object will be available in the global `window` object. - -### Register the buttons - -In order to display a PayPal Button component, we need to mount it in the DOM. - -```ts -// client only -window - .paypal? - .Buttons({/** configuration skipped */}) - .mount("#paypal-buttons-container") -// this script will mount the component in element with id="paypal-buttons-container" -``` - -## React on PayPal events - -Now, that the buttons are properly displayed, we need to react to two basic events. - -- `createOrder` -- `onApprove` - -There are additional events like `onInit`, `onCancel` or `onError` (and more) to be used on specific cases, which we are not going to cover in this guide. - -### `createOrder` event - -In the `creatOrder` callback, you need to prepare the PayPal order and return a token that identifies the order in PayPal. This token will be used later on to capture the payment. -It is called when the user clicks on the PayPal express checkout button. - -```ts{7-20} -// client only -window - .paypal? - .Buttons({ - createOrder: async ( - data: CreateOrderData, - actions: CreateOrderActions - ) => { - await setPaymentMethod(paypalMethod.value); - - await addToCart(); - - const response = await apiInstance.invoke.post<{ token: string }>( - "/store-api/paypal/express/create-order" - ); - return response?.data?.token; - }, - }) - .mount("#paypal-buttons-container") -``` - -The approach here is to set the payment method internally, then add a current product to the cart, and then prepare a PayPal token to be used later on. - -In the example above we do a couple of things: - -1. Set the payment method for the current context -2. Add a product to the cart -3. Create a PayPal order and return the token - -### `onApprove` event - -This event is called when the user approves the payment process. It's the last step before the payment is captured. - -```ts - ... - // part of window.paypal.Buttons({}) params - onApprove: async (data: OnApproveData, actions: OnApproveActions) => { - await apiInstance.invoke.post( - "/store-api/paypal/express/prepare-checkout", - { - token: data.orderID, - } - ); - // createOrder from useCheckout composable - orderCreated.value = await createOrder(); - refreshCart() - // apiInstance from useShopwareContext composable - const handlePaymentResponse = await apiInstance.invoke.post( - "/store-api/handle-payment", - { - orderId: orderCreated.value.id, - successUrl: `${window.location.origin}/order/success?order=${orderCreated.value.id}&success=true`, - errorUrl: `${window.location.origin}/order/success?order=${orderCreated.value.id}&success=false`, - } - ); - redirectPaymentUrl.value = handlePaymentResponse?.data?.redirectUrl; - // - }, - ... -``` - -The example above shows the code that is executed after a payer approves the PayPal popup. This function calls the `prepare-checkout` endpoint to register the upcoming PayPal transaction. - -::: tip Call custom endpoints -You can call custom endpoints using the `apiInstance.invoke.post()`, `.get()`, `.put()` or `.delete()` methods. They will automatically add the `sw-context-token` header to authenticate the request. -::: - -Thanks to the internal logic of the PayPal extension, the is already connected with the logged in customer. Now you can call `createOrder()` which creates an order through the Store-API. Once the order is created, its `id` can be used to invoke the `handle-payment` action to process payment. This action captures the money or redirects the user to an external payment gateway. - -## Working example - -The example shows the specific case, when a product can be bought in one action from the frontend. - - diff --git a/apps/docs/src/resources/integrations/payments/paypal.md b/apps/docs/src/resources/integrations/payments/paypal.md new file mode 100644 index 000000000..928545909 --- /dev/null +++ b/apps/docs/src/resources/integrations/payments/paypal.md @@ -0,0 +1,651 @@ +--- +head: + - - meta + - name: og:title + content: "PayPal Integration" + - - meta + - name: og:description + content: "In this chapter you will learn how to implement a custom payment flow based on PayPal Checkout." + - - meta + - name: og:image + content: "https://frontends-og-image.vercel.app/Paypal%20Checkout.png" +--- + + + +# PayPal Integration + +Paypal Logo + +:::tip Advanced Guide - prior knowledge required +In order to follow this guide properly, we recommend that you get familiar with the payment flow and payment API concepts first. + +- [Payment Flow in Shopware 6](https://developer.shopware.com/docs/concepts/commerce/checkout-concept/payments) +- [Payment API](https://shopware.stoplight.io/docs/store-api/8218801e50fe5-handling-the-payment) + ::: + +In this chapter you will learn how to integrate a payment flow with Shopware Frontends. There are various ways in which payment providers integrate with Shopware's API, so it is likely that you need to consult the documentation of your payment provider to get the details. + +This specific guides shows how to integrate the **PayPal Checkout** including **PayPal Express Checkout**. However, the general flow is the same for all payment providers, so you will be able to use this guide as a reference for different providers. + +Specifically, you will learn how to + +- Prepare the Shopware instance for taking PayPal payments +- Embed payment buttons in your frontend +- React on PayPal events to prepare and capture the payment + +## Install the payment extension + +Payment integrations require communication with the backend for various scenarios + +- Create a PayPal order +- Inform PayPal which payment was selected +- Capture the user payment after approval from the provider +- Update the order status +- Notify customers on successful/failed payment +- Other actions that need additional credentials which should stay hidden (i.e. secret authorization tokens) + +That's why the backend as a Payment middleware is a good option to store additional information, credentials, react on events and so on. + +:::tip +Make sure that the Payment Provider you would like to install, provides also an interface to interact via Store-API for headless solutions, specially when it's a synchronous payment flow. +::: + +The [SwagPayPal](https://github.com/shopware/SwagPayPal) extension is available on Shopware Cloud stores and also can be installed manually in self-managed instances. It provides useful endpoints to conduct payments with PayPal. We will be using two PayPal-specific endpoints in this guide: + +### Create order + +`/store-api/paypal/create-order` +`/store-api/paypal/express/create-order` (Express) + +- Creates an order directly with PayPal which contains information about the cart and the user +- Updates an existing order if given an order ID +- Returns a payment intent token that identifies the order in PayPal + +### Prepare checkout (Express) + +`/store-api/paypal/express/prepare-checkout` + +- Used after PayPal approved the payment process request +- Registers a customer based on PayPal account's data (name, address, email) and logs them in +- The API Client receives a new context token that points to the logged-in customer + +## Embed Payment buttons + +The next step is to embed the PayPal Checkout buttons in your frontend using the PayPal Javascript SDK. The SDK can be loaded from the PayPal CDN or using an npm package ([PayPal SDK Documentation](https://developer.paypal.com/sdk/js/configuration/)). In our example we're going to use the second option. + +### Load the PayPal SDK + +:::tip Client only +The PayPal SDK and all its methods should only be invoked on client side rendered pages. +::: + +In a Vue component we can use the `loadScript` method from the [`@paypal/paypal-js`](https://www.npmjs.com/package/@paypal/paypal-js) npm package: + +```ts +import { loadScript } from "@paypal/paypal-js"; + +loadScript({ + // client id is generated in the PayPal account's apps section + "client-id": + "AUAcLFoadrmy9JiW2cHgriy1mTy0MCqQOP_1SSeQEUArz_zPeF1VcNY2CCxcFBQpf_N4g1k5wFVNJ1Bk", + currency: "EUR", // or use some reference to the current currency + locale: "en_US", // as same as in the field above +}); +``` + +Now, the `paypal` object will be available in the global `window` object. + +Alternatively, the `loadScript` function returns a promise resolving to the paypal object. This can be useful if you want to load the script multiple times with different options. Note that you must delete `window.paypal` first. + +### Register the buttons + +In order to display a PayPal Button component, we need to mount it in the DOM. + +```ts +const divContainer = ref(); + +// client only +window + .paypal + .Buttons({/** configuration skipped */}) + .render(divContainer) +// this script will mount the component in element `divContainer` +``` + +## React on PayPal events + +Now, that the buttons are properly displayed, we need to react to two basic events. + +- `createOrder` +- `onApprove` + +There are additional events like `onInit`, `onClick`, `onCancel` or `onError` (and more) to be used on specific cases, which we are not going to cover in this guide. + +### `createOrder` event + +In the `creatOrder` callback, you need to prepare the PayPal order and return a token that identifies the order in PayPal. This token will be used later on to capture the payment. +It is called when the user clicks on the PayPal checkout button. + +```ts +const divContainer = ref(); + +// client only +window + .paypal + .Buttons({ + createOrder: async ( + data: CreateOrderData, + actions: CreateOrderActions + ) => { + const response = await apiClient.invoke( + "createPayPalOrder post /store-api/paypal/create-order" + ); + return response.data?.token; + }, + }) + .render(divContainer) +``` + +### `createOrder` event (Express) + +In the `creatOrder` callback, you need to prepare the PayPal order and return a token that identifies the order in PayPal. This token will be used later on to capture the payment. +It is called when the user clicks on the PayPal express checkout button. + +```ts +const divContainer = ref(); + +// client only +window + .paypal + .Buttons({ + createOrder: async ( + data: CreateOrderData, + actions: CreateOrderActions + ) => { + await setPaymentMethod(paypalMethod.value); + + await addToCart(); + + const response = await apiClient.invoke( + "createPayPalExpressOrder post /store-api/paypal/express/create-order" + ); + return response.data?.token; + }, + }) + .render(divContainer) +``` + +The approach here is to set the payment method internally, then add a current product to the cart, and then prepare a PayPal token to be used later on. + +In the example above we do a couple of things: + +1. Set the payment method for the current context +2. Add a product to the cart +3. Create a PayPal order and return the token + +### `onApprove` event + +This event is called when the user approves the payment process. It's the last step before the payment is captured. + +```ts + ... + // part of window.paypal.Buttons({}) params + onApprove: async (data: OnApproveData, actions: OnApproveActions) => { + // createOrder from useCheckout composable + orderCreated.value = await createOrder({ + paypalOrderId: data.orderID, + }); + refreshCart() + // apiClient from useShopwareContext composable + const handlePaymentResponse = await apiClient.invoke( + "handlePaymentMethod post /handle-payment", + { + query: { + paypalOrderId: data.orderID, + }, + body: { + orderId: order.id, + finishUrl: `${window.location.origin}/order/finish?order=${order.id}&success=true`, + }, + }, + ); + // call the /payment/finalize-transaction endpoint + await fetch(handlePaymentResponse.data.redirectUrl); + // ... + }, + ... +``` + +The example above shows the code that is executed after a payer approves the PayPal popup. This function calls `createOrder()` which creates an order through the Store-API. Once the order is created, its `id` can be used to invoke the `handle-payment` action to process payment. This action captures the money or redirects the user to an external payment gateway. + +### `onApprove` event (Express) + +This event is called when the user approves the payment process. It's the last step before the payment is captured. + +```ts + ... + // part of window.paypal.Buttons({}) params + onApprove: async (data: OnApproveData, actions: OnApproveActions) => { + await apiClient.invoke( + "preparePayPalExpressCheckout post /store-api/paypal/express/prepare-checkout", + { + body: { token: data.orderID }, + } + ); + // createOrder from useCheckout composable + const order = await createOrder({ paypalOrderId: data.orderID }); + refreshCart() + + // redirect to order confirmation site + + // - OR - one-click checkout + const handlePaymentResponse = await apiClient.invoke( + "handlePaymentMethod post /handle-payment", + { + query: { + isPayPalExpressCheckout: true, + paypalOrderId: data.orderID, + }, + body: { + orderId: order.id, + finishUrl: `${window.location.origin}/order/finish?order=${order.id}&success=true`, + }, + }, + ); + // call the /payment/finalize-transaction endpoint + await fetch(handlePaymentResponse.data.redirectUrl); + // ... + }, + ... +``` + +The example above shows the code that is executed after a payer approves the PayPal popup. This function calls the `prepare-checkout` endpoint to register the upcoming PayPal transaction. + +Thanks to the internal logic of the PayPal extension, the is already connected with the logged in customer. Now you can call `createOrder()` which creates an order through the Store-API. Once the order is created, its `id` can be used to invoke the `handle-payment` action to process payment. This action captures the money or redirects the user to an external payment gateway. + +## Working example (Express) + +The example shows the specific case, when a product can be bought in one action from the frontend. + + + +## Integrating other PayPal payment methods + +PayPal additionally provides Pay Later and Credit card (ACDC) alongside with a variety of alternative payment methods like Apple Pay, Google Pay or Venmo. +For reference check out [PayPal's documentation](https://developer.paypal.com/docs/checkout/) on integrating these. + +### Shared behaviour of `createOrder` and `onApprove` + +The `createOrder` and `onApprove` events are the same for all payment methods. +The only difference is the product used to create the order. + +```ts +async function createOrder(product?: 'paylater' | 'acdc' | 'applepay' | 'googlepay' | 'applepay' | 'venmo') { + const response = await apiClient.invoke( + "createPayPalOrder post /store-api/paypal/create-order", + { body: { product } }, + ); + + return response?.data?.token; +} + +async function onApprove(data: { orderID: string }) { + // createOrder from useCheckout composable + orderCreated.value = await createOrder({ + paypalOrderId: data.orderID, + }); + refreshCart() + // apiClient from useShopwareContext composable + const handlePaymentResponse = await apiClient.invoke( + "handlePaymentMethod post /handle-payment", + { + query: { + paypalOrderId: data.orderID, + }, + body: { + orderId: order.id, + finishUrl: `${window.location.origin}/order/finish?order=${order.id}&success=true`, + }, + }, + ); + // call the /payment/finalize-transaction endpoint + await fetch(handlePaymentResponse.data.redirectUrl); + ... +} +``` + +### Load the PayPal SDK including the additional payment methods + +Depending on the type of the payment method and how it integrates with PayPal, you need to add it to `enable-funding` or `components`: + +```ts +import { loadScript } from "@paypal/paypal-js"; + +loadScript({ + // Pay Later or venmo + "enable-funding": "paylater,venmo", + // ACDC, Apple Pay or Google Pay + components: "card-fields,applepay,googlepay", + ... +}); +``` + +### Pay Later + +```ts +const divContainer = ref(); + +window + .paypal + .Buttons({ + fundingSource: paypal.FUNDING.PAYLATER, + createOrder: createOrder.bind(this, "paylater"), + onApprove: onApprove.bind(this), + + // ... + }) + .render(divContainer) +``` + +### Venmo + +```ts +const divContainer = ref(); + +window + .paypal + .Buttons({ + fundingSource: paypal.FUNDING.VENMO, + createOrder: createOrder.bind(this, "venmo"), + onApprove: onApprove.bind(this), + + // ... + }) + .render(divContainer) +``` + +### Credit card (ACDC) + +```ts +const cardFields = paypal.CardFields({ + createOrder: createOrder.bind(this, "acdc"), + onApprove: onApprove.bind(this), + style: {/** some custom styling */}, +}) + +const nameField = cardFields.NameField({ + placeholder: "Card holder name", +}); +nameField.render("#acdc-name-field-container"); + +const numberField = cardFields.NumberField({ + placeholder: "Card number", +}); +numberField.render("#acdc-number-field-container"); + +const cvvField = cardFields.CVVField({ + placeholder: "Security code (CVV)", +}); +cvvField.render("#acdc-cvv-field-container"); + +const expiryField = cardFields.ExpiryField({ + placeholder: "Expiration date (MM/YY)", +}); +expiryField.render("#acdc-expiry-field-container"); +``` + +Upon form submit via your own rendered button you need to check the validity of the card fields: + +```ts +async function onFormSubmit() { + const cardState = await cardFields.getState(); + + if (state.isFormValid) { + // This will trigger the `onApprove` event + cardFields.submit(); + + return; + } + + // Do some advanced error handling, e.g. focus the invalid field + const firstInvalidFieldKey = Object.keys(state.fields).find((key) => !state.fields[key].isValid); + this.fields[firstInvalidFieldKey]?.focus(); +} +``` + +After submitting the card fields, the `onApprove` event will be triggered. + +### Google Pay + +For Google Pay to work, you need to load the Google Pay script in the head of your HTML document. + +```html + + + + +``` + +Now you can render the Google Pay button in your frontend: + +```ts +const { cart, totalPrice } = useCart(); +const { currency } = useSessionContext(); +const divContainer = ref(); + +async function renderGooglePay() { + if (!window?.google?.payments?.api?.PaymentsClient) { + throw new Error("Google Pay script is not load"); + } + + const { + isEligible, + apiVersion, + apiVersionMinor, + allowedPaymentMethods, + merchantInfo, + countryCode, + } = await window.paypal.Googlepay().config(); + + if (!isEligible) { + throw new Error("Funding for Google Pay is not eligible"); + } + + const gpClient = new window.google.payments.api.PaymentsClient({ + environment: "PRODUCTION", // or "TEST" + paymentDataCallbacks: { + onPaymentAuthorized: async (paymentData) => { + try { + await onPaymentAuthorized(paymentData); + return { transactionState: "SUCCESS" }; + } catch (e) { + return { + transactionState: "ERROR", + error: { intent: "PAYMENT_AUTHORIZATION", message: e.message || "TRANSACTION FAILED" }, + } + } + }, + }, + }); + + const { result } = await gpClient.isReadyToPay({ apiVersion, apiVersionMinor, allowedPaymentMethods }); + if (!result) { + throw new Error("Browser does not support Google Pay"); + } + + const paymentDataRequest = { + apiVersion, + apiVersionMinor, + allowedPaymentMethods, + merchantInfo, + callbackIntents: ["PAYMENT_AUTHORIZATION"], + transactionInfo: { + countryCode, + totalPriceStatus: "FINAL", + totalPriceLabel: "Grand Total", + currencyCode: currency.value.isoCode, + totalPrice: totalPrice.value, + displayItems: [ + { + label: "Subtotal", + price: cart.price.netPrice, + type: "SUBTOTAL", + }, + { + label: "Tax", + price: cart.price.calculatedTaxes.price, + type: "TAX", + } + ], + }, + }; + + gpClient.prefetchPaymentData(paymentDataRequest); + + const button = gpClient.createButton({ + allowedPaymentMethods, + onClick: () => { + // do some form validity checks before continue + + gpClient.loadPaymentData(paymentDataRequest).catch(); + }, + }); + + divContainer.appendChild(button); +} + +async function onPaymentAuthorized(paymentData) { + const orderId = await createOrder("googlepay"); + + if (!orderId) { + throw new Error("PayPal order could not be created") + } + + const confirmOrderResponse = await window.paypal.Googlepay().confirmOrder({ + orderId, + paymentMethodData: paymentData.paymentMethodData, + }); + + if (!["APPROVED","PAYER_ACTION_REQUIRED"].includes(confirmOrderResponse.status)) { + throw new Error("PayPal didn't approve the transaction."); + } + + if ("PAYER_ACTION_REQUIRED" === confirmOrderResponse.status) { + await window.paypal.Googlepay().initiatePayerAction({orderId}); + } + + this.onApprove({ orderId }); +} +``` + +### Apple Pay + +For Apple Pay to work, you need to load the Apple Pay script in the head of your HTML document. + +```html + + + + +``` + +Now you can render the Apple Pay button in your frontend: + +```ts +const { totalPrice } = useCart(); +const { activeBillingAddress } = useSessionContext(); +const divContainer = ref(); + +async function renderApplePay() { + if (!window.ApplePaySession?.supportsVersion(4) || !window.ApplePaySession?.canMakePayments()) { + throw new Error("Browser does not support Apple Pay"); + } + + const { + isEligible, + countryCode, + merchantCapabilities, + supportedNetworks, + currencyCode, + } = await window.paypal.Applepay().config(); + + if (!isEligible) { + throw new Error("Funding for Apple Pay is not eligible"); + } + + const billingContact = { + addressLines: [activeBillingAddress.street], + administrativeArea: activeBillingAddress.countryState?.name, + country: activeBillingAddress.country?.iso3, + countryCode: activeBillingAddress.country?.iso, + familyName: activeBillingAddress.lastName, + givenName: activeBillingAddress.firstName, + locality: activeBillingAddress.city, + postalCode: activeBillingAddress.zipcode, + } + + const paymentDataRequest = { + countryCode, + merchantCapabilities, + supportedNetworks, + currencyCode, + billingContact, + requiredShippingContactFields: [], + requiredBillingContactFields: [], + total: { + label: "TOTAL", + type: "final", + amount: totalPrice.value, + }, + }; + + const button = document.createElement("apple-pay-button"); + button.setAttribute("buttonStyle", "black"); + button.setAttribute("type", "buy"); + button.addEventListener("click",() => { + // do some form validity checks before continue + + const session = new window.ApplePaySession(4, paymentRequest); + + session.onvalidatemerchant = this.onValidateMerchant.bind(this, session); + session.onpaymentauthorized = this.onPaymentAuthorized.bind(this, session, billingContact); + + session.begin(); + }); + + divContainer.appendChild(button); +} + +async function onValidateMerchant(session, event) { + try { + const { merchantSession } = await window.paypal.Applepay().validateMerchant({ + validationUrl: event.validationURL, + }); + + session.completeMerchantValidation(merchantSession); + } catch (e) { + session.abort(); + } +} + +async function onPaymentAuthorized(session, billingContact, paymentData) { + try { + const orderId = await createOrder("applepay"); + + await paypal.Applepay().confirmOrder({ + orderId, + token: event.payment.token, + billingContact, + }); + + session.completePayment(window.ApplePaySession.STATUS_SUCCESS); + + this.onApprove({ orderId }); + } catch (e) { + session.abort(); + } +} +``` diff --git a/examples/express-checkout/README.md b/examples/express-checkout/README.md index 86e3a38cf..20bd79d3e 100644 --- a/examples/express-checkout/README.md +++ b/examples/express-checkout/README.md @@ -2,6 +2,8 @@ This example should help get you started developing [Shopware Frontends](https://github.com/shopware/frontends). +📖 Visit [Integrations > Payments > Paypal](https://frontends.shopware.com/resources/integrations/payments/paypal.html) docs for further information. + ## Customization - edit [./src/App.vue](./src/App.vue) in order to change the current example