UNPKG

@paypal/react-paypal-js

Version:
1,076 lines (909 loc) 99.9 kB
# react-paypal-js > React components for the [PayPal JS SDK](https://docs.paypal.ai/developer/how-to/sdk/js/v6/configuration) <div class="badges"> <a href="https://github.com/paypal/paypal-js/actions/workflows/main.yml"><img src="https://img.shields.io/github/actions/workflow/status/paypal/paypal-js/main.yml?branch=main&logo=github&style=flat-square" alt="build status"></a> <a href="https://www.npmjs.com/package/@paypal/react-paypal-js"><img src="https://img.shields.io/npm/v/@paypal/react-paypal-js.svg?style=flat-square" alt="npm version"></a> <a href="https://bundlephobia.com/result?p=@paypal/react-paypal-js"><img src="https://img.shields.io/bundlephobia/minzip/@paypal/react-paypal-js.svg?style=flat-square" alt="bundle size"></a> <a href="https://www.npmtrends.com/@paypal/react-paypal-js"><img src="https://img.shields.io/npm/dm/@paypal/react-paypal-js.svg?style=flat-square" alt="npm downloads"></a> <a href="https://github.com/paypal/react-paypal-js/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@paypal/react-paypal-js.svg?style=flat-square" alt="apache license"></a> <a href="https://paypal.github.io/paypal-js/web-sdk-v6-react-storybook/"><img src="https://raw.githubusercontent.com/storybooks/brand/master/badge/badge-storybook.svg" alt="storybook"></a> </div> --- > **Are you still using the old PayPal JS SDK V5 SDK?** > > This documentation teaches how to use the latest PayPal JS SDK with react. For the integration using PayPal JS SDK V5 with `PayPalScriptProvider`, `PayPalButtons`, `PayPalHostedFields`, and `BraintreePayPalButtons`, see [README-PAYPAL-JS-SDK-V5.md](./README-PAYPAL-JS-SDK-V5.md). --- ## Why use react-paypal-js? ### The Problem Integrating PayPal into React applications requires careful handling of SDK script loading, payment session management, and UI rendering. Building a robust integration from scratch can lead to issues with timing, state management, and buyer experience. ### The Solution `react-paypal-js` provides a modern, hooks-based solution that abstracts away the complexities of the PayPal V6 SDK. It enforces best practices by default to ensure buyers get the best possible user experience. **Features** - **Modern Hooks API** - Fine-grained control over payment sessions with `usePayPalOneTimePaymentSession`, `useVenmoOneTimePaymentSession`, and more - **Built-in Eligibility** - Automatically check which payment methods are available with `useEligibleMethods()` - **Web Component Buttons** - Use PayPal's optimized `<paypal-button>`, `<venmo-button>`, and `<paypal-pay-later-button>` web components - **Flexible Loading** - Support for string token/id, Promise-based token/id, and deferred loading patterns - **TypeScript Support** - Complete type definitions for all components and hooks - **SSR Compatible** - Built-in hydration handling for server-side rendered applications ## Supported Payment Methods - **PayPal** - Standard PayPal checkout - **Venmo** - Venmo payments - **Pay Later** - PayPal's buy now, pay later option - **PayPal Basic Card** - Guest card payments without a PayPal account - **PayPal Advanced Card** - Card payments with enhanced features and customization options - **PayPal Subscriptions** - Recurring billing subscriptions - **PayPal Save** - Vault payment methods without purchase - **PayPal Credit** - PayPal Credit one-time and save payments - **Google Pay** - Native Google Pay button flow through PaymentsClient - **Apple Pay** - Native Apple Pay payments (Safari + HTTPS only) ## Resources - [PayPal V6 SDK Documentation](https://docs.paypal.ai/payments/methods/paypal/sdk/js/v6/paypal-checkout) - [React Sample Integration](https://github.com/paypal-examples/v6-web-sdk-sample-integration/tree/main/client/prebuiltPages/react) - Full working example with Node.js backend - [Live Demo](https://v6-web-sdk-sample-integration-server.fly.dev/client/prebuiltPages/react/dist/) - Try the sample integration in sandbox mode - [PayPal Server SDK](https://www.npmjs.com/package/@paypal/paypal-server-sdk) - For backend integration - [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/) - [PayPal Sandbox Test Accounts](https://developer.paypal.com/dashboard/accounts) - [PayPal Sandbox Card Testing](https://developer.paypal.com/tools/sandbox/card-testing/) ## Installation ```sh npm install @paypal/react-paypal-js ``` ## Quick Start ```tsx import { PayPalProvider, PayPalOneTimePaymentButton, } from "@paypal/react-paypal-js/sdk-v6"; function App() { return ( <PayPalProvider clientId="your-client-id" environment="sandbox" components={["paypal-payments"]} pageType="checkout" > <CheckoutPage /> </PayPalProvider> ); } function CheckoutPage() { return ( <PayPalOneTimePaymentButton createOrder={async () => { const response = await fetch("/api/create-order", { method: "POST", }); const { orderId } = await response.json(); return { orderId }; }} onApprove={async ({ orderId }: OnApproveDataOneTimePayments) => { await fetch(`/api/capture-order/${orderId}`, { method: "POST", }); console.log("Payment captured!"); }} /> ); } ``` ## PayPalProvider The `PayPalProvider` component is the entry point for the V6 SDK. It handles loading the PayPal SDK, creating an instance, and running eligibility checks. ### Props | Prop | Type | Required | Description | | ------------------------- | ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------ | | `clientToken` | `string \| Promise<string>` | \* | Client token from your server. Mutually exclusive with `clientId`. | | `clientId` | `string \| Promise<string>` | \* | Client ID from your PayPal app. Mutually exclusive with `clientToken`. | | `components` | `Components[]` | No | SDK components to load. Defaults to `["paypal-payments"]`. | | `pageType` | `string` | No | Type of page: `"checkout"`, `"product-details"`, `"cart"`, `"product-listing"`, etc. | | `locale` | `string` | No | Locale for the SDK (e.g., `"en_US"`). | | `environment` | `"sandbox" \| "production"` | **Yes** | **Required.** SDK environment. `clientId` does not select the environment in v6 — this prop does. | | `merchantId` | `string \| string[]` | No | PayPal merchant ID(s). | | `clientMetadataId` | `string` | No | Client metadata ID for tracking. | | `partnerAttributionId` | `string` | No | Partner attribution ID (BN code). | | `shopperSessionId` | `string` | No | Shopper session ID for tracking. | | `testBuyerCountry` | `string` | No | Test buyer country code (sandbox only). | | `debug` | `boolean` | No | Enable debug mode. | | `dataNamespace` | `string` | No | Custom namespace for the SDK script data attribute. | | `eligibleMethodsResponse` | `FindEligiblePaymentMethodsResponse` | No | Server-fetched eligibility response for SDK hydration (see [Server-Side Rendering](#server-side-rendering)). | > \* Either `clientToken` or `clientId` is required, but not both. They are mutually exclusive. ### Available Components The `components` prop accepts an array of the following values: - `"paypal-payments"` - PayPal and Pay Later buttons - `"venmo-payments"` - Venmo button - `"paypal-guest-payments"` - Guest checkout (card payments) - `"paypal-subscriptions"` - Subscription payments - `"card-fields"` - Card Fields (advanced card payment UI) - `"googlepay-payments"` - Google Pay ### With Promise-based Client ID ```tsx function App() { // Memoize to prevent re-fetching on each render const clientIdPromise = useMemo(() => fetchClientId(), []); return ( <PayPalProvider clientId={clientIdPromise} environment="sandbox" components={["paypal-payments"]} pageType="checkout" > <CheckoutPage /> </PayPalProvider> ); } ``` Alternative: With Promise-based Client Token ```tsx function App() { // Memoize to prevent re-fetching on each render const tokenPromise = useMemo(() => fetchClientToken(), []); return ( <PayPalProvider clientToken={tokenPromise} environment="sandbox" components={["paypal-payments"]} pageType="checkout" > <CheckoutPage /> </PayPalProvider> ); } ``` ### Deferred Loading ```tsx function App() { const [clientId, setClientId] = useState<string>(); useEffect(() => { fetchClientId().then(setClientId); }, []); return ( <PayPalProvider clientId={clientId} environment="sandbox" components={["paypal-payments"]} pageType="checkout" > <CheckoutPage /> </PayPalProvider> ); } ``` ### Tracking Loading State Use the `usePayPal` hook to access the SDK loading status: ```tsx import { usePayPal, INSTANCE_LOADING_STATE, } from "@paypal/react-paypal-js/sdk-v6"; function CheckoutPage() { const { loadingStatus, error } = usePayPal(); if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) { return <div className="spinner">Loading PayPal...</div>; } if (loadingStatus === INSTANCE_LOADING_STATE.REJECTED) { return ( <div className="error">Failed to load PayPal SDK: {error?.message}</div> ); } return <PayPalOneTimePaymentButton orderId="ORDER-123" />; } ``` ## Button Components ### PayPalOneTimePaymentButton Renders a PayPal button for one-time payments. ```tsx import { PayPalOneTimePaymentButton } from "@paypal/react-paypal-js/sdk-v6"; <PayPalOneTimePaymentButton createOrder={async () => { const response = await fetch("/api/create-order", { method: "POST" }); const { orderId } = await response.json(); return { orderId }; }} onApprove={async ({ orderId }: OnApproveDataOneTimePayments) => { await fetch(`/api/capture/${orderId}`, { method: "POST" }); console.log("Payment approved!"); }} onCancel={(data: OnCancelDataOneTimePayments) => console.log("Payment cancelled") } onError={(data: OnErrorData) => console.error("Payment error:", data)} onComplete={(data: OnCompleteData) => console.log("Payment Flow Completed")} />; ``` **Props:** | Prop | Type | Description | | ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------- | | `orderId` | `string` | Static order ID (alternative to `createOrder`) | | `createOrder` | `() => Promise<{ orderId: string }>` | Async function to create an order | | `presentationMode` | `"auto" \| "popup" \| "modal" \| "redirect"` | Optional. How to present the payment session. Defaults to `"auto"`. | | `onApprove` | `(data) => void` | Called when payment is approved | | `onCancel` | `() => void` | Called when buyer cancels | | `onError` | `(error) => void` | Called on error | | `onComplete` | `(data) => void` | Called when payment session completes | | `type` | `"pay" \| "checkout" \| "buynow" \| "donate" \| "subscribe"` | Button label type | | `disabled` | `boolean` | Disable the button | ### VenmoOneTimePaymentButton Renders a Venmo button for one-time payments. Requires `"venmo-payments"` in the provider's `components` array. ```tsx import { VenmoOneTimePaymentButton } from "@paypal/react-paypal-js/sdk-v6"; <PayPalProvider clientId={clientId} environment="sandbox" components={["paypal-payments", "venmo-payments"]} pageType="checkout" > <VenmoOneTimePaymentButton createOrder={async () => { const { orderId } = await createOrder(); return { orderId }; }} onApprove={(data: OnApproveDataOneTimePayments) => console.log("Venmo payment approved!", data) } onCancel={(data: OnCancelDataOneTimePayments) => console.log("Venmo payment cancelled", data) } onError={(data: OnErrorData) => console.error("Venmo payment error:", data)} onComplete={(data: OnCompleteData) => console.log("Venmo payment flow completed", data) } /> </PayPalProvider>; ``` ### GooglePayOneTimePaymentButton Renders a native Google Pay button for one-time payments. Requires `"googlepay-payments"` in the provider's `components` array. Google Pay prerequisites: 1. Load Google Pay JS in your app HTML shell (for example `public/index.html`): ```html <script async src="https://pay.google.com/gp/p/js/pay.js"></script> ``` 2. Ensure the script is available before rendering `GooglePayOneTimePaymentButton`, since this component depends on `window.google.payments.api.PaymentsClient`. ```tsx import { PayPalProvider, GooglePayOneTimePaymentButton, useEligibleMethods, INSTANCE_LOADING_STATE, usePayPal, } from "@paypal/react-paypal-js/sdk-v6"; function GooglePayCheckout() { const { loadingStatus } = usePayPal(); const { eligiblePaymentMethods, isLoading } = useEligibleMethods({ payload: { currencyCode: "USD" }, }); if (loadingStatus === INSTANCE_LOADING_STATE.PENDING || isLoading) { return <div>Loading Google Pay...</div>; } const googlePayConfig = eligiblePaymentMethods?.isEligible("googlepay") ? eligiblePaymentMethods.getDetails("googlepay").config : null; if (!googlePayConfig) { return <div>Google Pay is not eligible for this buyer.</div>; } return ( <GooglePayOneTimePaymentButton googlePayConfig={googlePayConfig} transactionInfo={{ countryCode: "US", currencyCode: "USD", totalPriceStatus: "FINAL", totalPrice: "100.00", }} createOrder={async () => { const response = await fetch("/api/create-order", { method: "POST" }); const { orderId } = await response.json(); return { orderId }; }} onApprove={(data) => console.log("Google Pay approved", data)} onCancel={() => console.log("Google Pay cancelled")} onError={(error) => console.error("Google Pay error", error)} buttonType="pay" buttonColor="default" buttonSizeMode="fill" /> ); } function App() { return ( <PayPalProvider clientId="your-client-id" environment="sandbox" components={["googlepay-payments"]} pageType="checkout" > <GooglePayCheckout /> </PayPalProvider> ); } ``` **Props:** | Prop | Type | Description | | ----------------- | ---------------------------------------- | -------------------------------------------------------------------------------------- | | `googlePayConfig` | `GooglePayConfigFromFindEligibleMethods` | Google Pay config returned by `eligiblePaymentMethods.getDetails("googlepay")` | | `transactionInfo` | `GooglePayTransactionInfo` | Google Pay transaction details (country, currency, amount, and optional display items) | | `createOrder` | `() => Promise<{ orderId: string }>` | Async function to create an order | | `onApprove` | `(data) => void \| Promise<void>` | Called when Google Pay payment is approved | | `onCancel` | `() => void` | Called when buyer cancels the Google Pay sheet | | `onError` | `(error: Error) => void` | Called on setup or payment errors | | `environment` | `"TEST" \| "PRODUCTION"` | Google Pay environment (default: `"TEST"`) | | `buttonType` | `"pay" \| ...` | Google Pay button type | | `buttonColor` | `"default" \| "black" \| "white"` | Google Pay button color | | `buttonSizeMode` | `"fill" \| "static"` | Google Pay button size mode | | `buttonLocale` | `string` | Google Pay button locale | | `disabled` | `boolean` | Disable interaction | ### PayLaterOneTimePaymentButton Renders a Pay Later button for financing options. Country code and product code are automatically populated from eligibility data, so eligibility must be fetched first — via `useEligibleMethods()` client-side (shown below) or the provider's `eligibleMethodsResponse` prop server-side. ```tsx import { PayLaterOneTimePaymentButton, useEligibleMethods, } from "@paypal/react-paypal-js/sdk-v6"; function PayLaterCheckout() { // Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse) const { eligiblePaymentMethods, isLoading } = useEligibleMethods({ payload: { purchase_units: [{ amount: { currency_code: "USD" } }] }, }); if (isLoading) { return <Spinner />; } if (!eligiblePaymentMethods?.isEligible("paylater")) { return null; } return ( <PayLaterOneTimePaymentButton createOrder={async () => { const { orderId } = await createOrder(); return { orderId }; }} onApprove={(data: OnApproveDataOneTimePayments) => console.log("Pay Later approved!", data) } onCancel={(data: OnCancelDataOneTimePayments) => console.log("Pay Later cancelled", data) } onError={(data: OnErrorData) => console.error("Pay Later error:", data)} onComplete={(data: OnCompleteData) => console.log("Pay Later flow completed", data) } /> ); } ``` ### PayPalGuestPaymentButton Renders a guest checkout button for card payments without a PayPal account (Branded Card/Debit Card checkout). Requires `"paypal-guest-payments"` in the provider's `components` array. ```tsx import { PayPalGuestPaymentButton } from "@paypal/react-paypal-js/sdk-v6"; <PayPalProvider clientId={clientId} environment="sandbox" components={["paypal-payments", "paypal-guest-payments"]} pageType="checkout" > <PayPalGuestPaymentButton createOrder={async () => { const { orderId } = await createOrder(); return { orderId }; }} onApprove={(data: OnApproveDataOneTimePayments) => console.log("Guest payment approved!", data) } onCancel={(data: OnCancelDataOneTimePayments) => console.log("Guest payment cancelled", data) } onError={(data: OnErrorData) => console.error("Guest payment error:", data)} onComplete={(data: OnCompleteData) => console.log("Guest payment flow completed", data) } /> </PayPalProvider>; ``` ### PayPalSavePaymentButton Renders a button for vaulting a payment method without making a purchase. ```tsx import { PayPalSavePaymentButton } from "@paypal/react-paypal-js/sdk-v6"; <PayPalSavePaymentButton createVaultToken={async () => { const response = await fetch("/api/create-vault-token", { method: "POST", }); const { vaultSetupToken } = await response.json(); return { vaultSetupToken }; }} onApprove={({ vaultSetupToken }: OnApproveDataSavePayments) => { console.log("Payment method saved:", vaultSetupToken); }} onCancel={(data: OnCancelDataSavePayments) => console.log("Save payment cancelled", data) } onError={(data: OnErrorData) => console.error("Save payment error:", data)} onComplete={(data: OnCompleteData) => console.log("Save payment flow completed", data) } />; ``` ### PayPalSubscriptionButton Renders a PayPal button for subscription payments. Requires `"paypal-subscriptions"` in the provider's `components` array. ```tsx import { PayPalSubscriptionButton } from "@paypal/react-paypal-js/sdk-v6"; <PayPalProvider clientId={clientId} environment="sandbox" components={["paypal-subscriptions"]} pageType="checkout" > <PayPalSubscriptionButton createSubscription={async () => { const response = await fetch("/api/create-subscription", { method: "POST", }); const { subscriptionId } = await response.json(); return { subscriptionId }; }} onApprove={(data: OnApproveDataOneTimePayments) => console.log("Subscription approved:", data) } onCancel={(data: OnCancelDataOneTimePayments) => console.log("Subscription cancelled", data) } onError={(data: OnErrorData) => console.error("Subscription error:", data)} onComplete={(data: OnCompleteData) => console.log("Subscription flow completed", data) } /> </PayPalProvider>; ``` ### PayPalCreditOneTimePaymentButton Renders a PayPal Credit button for one-time payments. The `countryCode` is automatically populated from eligibility data, so eligibility must be fetched first — via `useEligibleMethods()` client-side (shown below) or the provider's `eligibleMethodsResponse` prop server-side. ```tsx import { PayPalCreditOneTimePaymentButton, useEligibleMethods, } from "@paypal/react-paypal-js/sdk-v6"; function CreditCheckout() { // Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse) const { eligiblePaymentMethods, isLoading } = useEligibleMethods({ payload: { purchase_units: [{ amount: { currency_code: "USD" } }] }, }); if (isLoading) { return <Spinner />; } if (!eligiblePaymentMethods?.isEligible("credit")) { return null; } return ( <PayPalCreditOneTimePaymentButton createOrder={async () => { const response = await fetch("/api/create-order", { method: "POST" }); const { orderId } = await response.json(); return { orderId }; }} onApprove={({ orderId }: OnApproveDataOneTimePayments) => console.log("Credit payment approved:", orderId) } onCancel={(data: OnCancelDataOneTimePayments) => console.log("Credit payment cancelled", data) } onError={(data: OnErrorData) => console.error("Credit payment error:", data) } onComplete={(data: OnCompleteData) => console.log("Credit payment flow completed", data) } /> ); } ``` ### PayPalCreditSavePaymentButton Renders a PayPal Credit button for saving a credit payment method (vaulting). The `countryCode` is automatically populated from eligibility data, so eligibility must be fetched first — via `useEligibleMethods()` client-side (shown below) or the provider's `eligibleMethodsResponse` prop server-side. ```tsx import { PayPalCreditSavePaymentButton, useEligibleMethods, } from "@paypal/react-paypal-js/sdk-v6"; function CreditSaveCheckout() { // Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse) const { eligiblePaymentMethods, isLoading } = useEligibleMethods({ payload: { purchase_units: [{ amount: { currency_code: "USD" } }] }, }); if (isLoading) { return <Spinner />; } if (!eligiblePaymentMethods?.isEligible("credit")) { return null; } return ( <PayPalCreditSavePaymentButton createVaultToken={async () => { const response = await fetch("/api/create-vault-token", { method: "POST", }); const { vaultSetupToken } = await response.json(); return { vaultSetupToken }; }} onApprove={(data: OnApproveDataSavePayments) => console.log("Credit saved:", data) } onCancel={(data: OnCancelDataSavePayments) => console.log("Credit save cancelled", data) } onError={(data: OnErrorData) => console.error("Credit save error:", data)} onComplete={(data: OnCompleteData) => console.log("Credit save flow completed", data) } /> ); } ``` ### ApplePayOneTimePaymentButton Renders Apple's native `<apple-pay-button>` web component and manages the full Apple Pay payment flow — including merchant validation, payment authorization, and order confirmation — via the PayPal SDK. **Requirements:** - Safari browser (macOS 10.12+ / iOS 10+) - HTTPS connection - Apple Pay configured on the user's device - `components={["applepay-payments"]}` in `PayPalProvider` - Apple Pay JS SDK loaded via a script tag in your HTML: ```html <script crossorigin src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js" ></script> ``` **Integration steps:** 1. Check `window.ApplePaySession?.canMakePayments()` — only render the button if this returns `true`. Wrap in `try-catch` because it throws on non-HTTPS connections. 2. Call `useEligibleMethods()` to fetch eligibility and obtain `applePayConfig` from `getDetails("applepay").config`. 3. Pass `applePayConfig` explicitly to the component — it is a required prop. ```tsx import { PayPalProvider, ApplePayOneTimePaymentButton, useEligibleMethods, } from "@paypal/react-paypal-js/sdk-v6"; async function createOrder() { const response = await fetch("/api/paypal/create-order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [{ id: "item-1", quantity: 1 }], }), }); const data = await response.json(); return { orderId: data.id }; } async function onApprove(data) { // confirmOrder is handled internally by the hook. // Capture the order using the ID from the confirmation response. const orderId = data.approveApplePayPayment.id; const response = await fetch(`/api/paypal/capture/${orderId}`, { method: "POST", }); const result = await response.json(); console.log("Apple Pay payment captured:", result); } function ApplePayCheckout() { // Step 1: Check if Apple Pay is supported by the browser/device. // canMakePayments() throws on non-HTTPS, so wrap in try-catch. let canUseApplePay = false; try { canUseApplePay = typeof window !== "undefined" && !!window.ApplePaySession?.canMakePayments(); } catch { // Not available (e.g., non-HTTPS environment) } // Step 2: Fetch eligibility. // Note: hooks must be called unconditionally (React rules of hooks). // To avoid the eligibility API call on unsupported browsers, split the // check and the button into separate components in your app. const { eligiblePaymentMethods, isLoading, error } = useEligibleMethods({ payload: { currencyCode: "USD" }, }); if (!canUseApplePay) { return <div>Apple Pay is not available in this browser.</div>; } if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } // Step 3: Check merchant eligibility and get config. const isEligible = eligiblePaymentMethods?.isEligible("applepay"); if (!isEligible) { return <div>Apple Pay is not eligible.</div>; } const applePayConfig = eligiblePaymentMethods?.getDetails("applepay")?.config; if (!applePayConfig) { return null; } return ( <ApplePayOneTimePaymentButton applePayConfig={applePayConfig} paymentRequest={{ countryCode: "US", currencyCode: "USD", requiredBillingContactFields: [ "name", "phone", "email", "postalAddress", ], requiredShippingContactFields: [], total: { label: "Demo (Card is not charged)", amount: "20.00", type: "final", }, }} createOrder={createOrder} onApprove={onApprove} onCancel={() => console.log("Apple Pay cancelled")} onError={(error) => console.error("Apple Pay error:", error)} applePaySessionVersion={4} buttonstyle="black" type="buy" /> ); } export default function App() { return ( <PayPalProvider clientId="YOUR_CLIENT_ID" environment="sandbox" components={["applepay-payments"]} pageType="checkout" > <ApplePayCheckout /> </PayPalProvider> ); } ``` **Props:** | Prop | Type | Required | Description | | ------------------------ | -------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | | `applePayConfig` | `ApplePayConfig` | Yes | Config object from `useEligibleMethods().getDetails("applepay").config` | | `paymentRequest` | `ApplePayPaymentRequest` | Yes | Apple Pay payment request (countryCode, currencyCode, total, etc.) | | `createOrder` | `() => Promise<{ orderId: string }>` | Yes | Called during authorization to create the PayPal order | | `onApprove` | `(data: ConfirmOrderResponse) => void` | Yes | Called after payment confirmation; use `data.approveApplePayPayment.id` to capture | | `onCancel` | `() => void` | No | Called when the buyer dismisses the payment sheet | | `onError` | `(error: Error) => void` | No | Called on errors (merchant validation failure, network error, etc.) | | `applePaySessionVersion` | `number` | No | Apple Pay JS API version passed to `ApplePaySession` (minimum: 4) | | `buttonstyle` | `"black" \| "white" \| "white-outline"` | No | Visual style of the Apple Pay button | | `type` | `"pay" \| "buy" \| "set-up" \| "donate" \| "check-out" \| "book" \| "subscribe"` | No | Label displayed on the button | | `locale` | `string` | No | Locale for the button label (e.g., `"en"`, `"fr"`, `"ja"`) | | `disabled` | `boolean` | No | Disables the button | **Key differences from other PayPal buttons:** - No `presentationMode` — Apple controls the native payment sheet UI - No eager order creation (`orderId` prop) — orders are always created lazily during payment authorization - `applePayConfig` is required and must be obtained from `useEligibleMethods()` - `onApprove` receives `ConfirmOrderResponse` — capture the order using `data.approveApplePayPayment.id` ## Braintree PayPal Integration Braintree merchants use `BraintreePayPalProvider` instead of `PayPalProvider` to integrate PayPal via Braintree's [`paypalCheckoutV6`](https://braintree.github.io/braintree-web/current/PayPalCheckoutV6.html) module. This provider initializes the Braintree client, creates a PayPal Checkout V6 instance, and loads the PayPal SDK — then exposes the instance to child components and hooks via React context. **Resources:** - [Braintree PayPalCheckoutV6 API Reference](https://braintree.github.io/braintree-web/current/PayPalCheckoutV6.html) — full method signatures, options, and type definitions - [Sample Integration Repo](https://github.com/paypal-examples/v6-web-sdk-with-braintree-sdk-sample-integration) — working Braintree + React example with setup instructions - [Braintree Server SDK Guide](https://developer.paypal.com/braintree/docs/start/hello-server) — server-side nonce processing ### Prerequisites - A Braintree merchant account with PayPal enabled - A Braintree **client token** generated server-side via the [Braintree SDK](https://developer.paypal.com/braintree/docs/start/hello-server). See the [sample integration's gateway setup](https://github.com/paypal-examples/v6-web-sdk-with-braintree-sdk-sample-integration/blob/main/server/node/src/braintreeServerSdkClient.ts) and [client token route handler](https://github.com/paypal-examples/v6-web-sdk-with-braintree-sdk-sample-integration/blob/main/server/node/src/routes/authRouteHandler.ts) for a Node.js example. - The Braintree Web `client` and `paypal-checkout-v6` scripts loaded before rendering: ```html <script src="https://js.braintreegateway.com/web/3.142.0/js/client.min.js"></script> <script src="https://js.braintreegateway.com/web/3.142.0/js/paypal-checkout-v6.min.js"></script> ``` ### BraintreePayPalProvider Wraps child components with Braintree context. On mount it validates the namespace, creates a Braintree client instance, creates a `paypalCheckoutV6` instance, and calls `loadPayPalSDK()`. On unmount it calls `teardown()` to release resources. ```tsx import { useState, useEffect } from "react"; import { BraintreePayPalProvider } from "@paypal/react-paypal-js/sdk-v6"; declare global { interface Window { braintree: BraintreeV6Namespace; } } function App() { const [clientToken, setClientToken] = useState<string | undefined>(undefined); useEffect(() => { fetch("/auth/browser-safe-client-token") .then((res) => res.json()) .then(({ clientToken }) => setClientToken(clientToken)); }, []); if (!clientToken) { return <div>Loading...</div>; } return ( <BraintreePayPalProvider namespace={window.braintree} braintreeClientToken={clientToken} > <CheckoutPage /> </BraintreePayPalProvider> ); } ``` **Props:** | Prop | Type | Required | Description | | ---------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `namespace` | `BraintreeV6Namespace` | Yes | The `braintree` global namespace — must expose `client.create` and `paypalCheckoutV6.create` functions | | `braintreeClientToken` | `string \| undefined` | Yes | Client token from your server (generated via the Braintree SDK) | | `children` | `ReactNode` | Yes | Child components | > **Note:** The `namespace` prop must have referential stability across renders. An unstable reference (e.g., creating the object inline) will cause re-initialization on every render. Use a module-level constant, `useRef`, or `useMemo`. ### BraintreePayPalOneTimePaymentButton Renders a `<paypal-button>` web component for one-time Braintree PayPal payments. Internally uses `useBraintreePayPalOneTimePaymentSession` to create and start payment sessions. ```tsx import { BraintreePayPalProvider, BraintreePayPalOneTimePaymentButton, useBraintreePayPal, } from "@paypal/react-paypal-js/sdk-v6"; import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6"; function CheckoutPage() { const { braintreePayPalCheckoutInstance } = useBraintreePayPal(); return ( <BraintreePayPalOneTimePaymentButton amount="100.00" currency="USD" intent="capture" type="pay" onApprove={async (data: BraintreeApprovalData) => { const { nonce } = await braintreePayPalCheckoutInstance!.tokenizePayment(data); // Send nonce to your server await fetch("/api/braintree/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nonce }), }); }} onCancel={(data) => console.log("Cancelled", data)} onError={(err) => console.error("Error", err)} /> ); } function App() { const [clientToken, setClientToken] = useState<string | undefined>(undefined); useEffect(() => { fetch("/auth/browser-safe-client-token") .then((res) => res.json()) .then(({ clientToken }) => setClientToken(clientToken)); }, []); if (!clientToken) { return <div>Loading...</div>; } return ( <BraintreePayPalProvider namespace={window.braintree} braintreeClientToken={clientToken} > <CheckoutPage /> </BraintreePayPalProvider> ); } ``` **Props:** | Prop | Type | Required | Description | | ------------------------- | ------------------------------------------------------------- | -------- | ----------------------------------------------------------------- | | `amount` | `string` | Yes | Payment amount (e.g., `"100.00"`) | | `currency` | `string` | Yes | ISO 4217 currency code (e.g., `"USD"`) | | `onApprove` | `(data: BraintreeApprovalData) => Promise<void>` | Yes | Called when buyer approves — tokenize the payment here | | `intent` | `"authorize" \| "capture" \| "order"` | No | Payment intent (default: `"capture"`) | | `commit` | `boolean` | No | `true` for "Pay Now", `false` for "Continue" | | `offerCredit` | `boolean` | No | Offer PayPal Credit as default funding | | `onCancel` | `(data: BraintreeOnCancelData) => void` | No | Called when buyer cancels | | `onError` | `(err: Error) => void` | No | Called on errors | | `onShippingAddressChange` | `(data: BraintreeShippingAddressChangeData) => Promise<void>` | No | Called when buyer changes shipping address | | `onShippingOptionsChange` | `(data: BraintreeShippingOptionsChangeData) => Promise<void>` | No | Called when buyer selects a shipping option | | `lineItems` | `BraintreeLineItem[]` | No | Line items for the transaction | | `shippingOptions` | `BraintreeShippingOption[]` | No | Available shipping options | | `amountBreakdown` | `BraintreeAmountBreakdown` | No | Breakdown of the total amount (item total, shipping, tax, etc.) | | `userAuthenticationEmail` | `string` | No | Pre-fill the PayPal login email | | `displayName` | `string` | No | Merchant name displayed in the PayPal lightbox | | `presentationMode` | `BraintreePresentationMode` | No | UI mode: `"auto"`, `"popup"`, `"modal"`, `"redirect"`, etc. | | `returnUrl` | `string` | No | Return URL (required for `"direct-app-switch"` presentation mode) | | `cancelUrl` | `string` | No | Cancel URL (required for `"direct-app-switch"` presentation mode) | | `type` | `"pay" \| "checkout" \| "buynow" \| "donate" \| "subscribe"` | No | Button label type (default: `"pay"`) | | `disabled` | `boolean` | No | Disable the button | ### BraintreePayPalBillingAgreementButton Renders a `<paypal-button>` for vault-only flows — saving a buyer's PayPal account as a payment method without an immediate charge. Supports subscription plans via `planType` and `planMetadata`. ```tsx import { BraintreePayPalBillingAgreementButton, useBraintreePayPal, } from "@paypal/react-paypal-js/sdk-v6"; import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6"; function BillingAgreementButton() { const { braintreePayPalCheckoutInstance } = useBraintreePayPal(); return ( <BraintreePayPalBillingAgreementButton type="subscribe" billingAgreementDescription="Monthly subscription to Premium" planType="SUBSCRIPTION" planMetadata={{ currencyIsoCode: "USD", name: "Premium Plan", billingCycles: [ { billingFrequency: 1, billingFrequencyUnit: "MONTH", numberOfExecutions: 0, sequence: 1, startDate: "2026-07-01T00:00:00Z", trial: false, pricingScheme: { pricingModel: "FIXED", price: "9.99" }, }, ], }} onApprove={async (data: BraintreeApprovalData) => { const { nonce } = await braintreePayPalCheckoutInstance!.tokenizePayment(data); await fetch("/api/braintree/vault", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nonce }), }); }} onCancel={(data) => console.log("Cancelled", data)} onError={(err) => console.error("Error", err)} /> ); } ``` **Props:** | Prop | Type | Required | Description | | ----------------------------- | ------------------------------------------------------------------ | -------- | -------------------------------------------------------------- | | `onApprove` | `(data: BraintreeApprovalData) => Promise<void>` | Yes | Called when buyer approves — tokenize with `data.billingToken` | | `billingAgreementDescription` | `string` | No | Description shown to the buyer (e.g., "Monthly subscription") | | `planType` | `"RECURRING" \| "SUBSCRIPTION" \| "UNSCHEDULED" \| "INSTALLMENTS"` | No | Type of billing plan | | `planMetadata` | `BraintreePlanMetadata` | No | Subscription plan details including billing cycles | | `amount` | `string` | No | Amount for vault-with-purchase flows | | `currency` | `string` | No | Currency for vault-with-purchase flows | | `offerCredit` | `boolean` | No | Offer PayPal Credit | | `userAction` | `"CONTINUE" \| "COMMIT" \| "SETUP_NOW"` | No | Button action label | | `displayName` | `string` | No | Merchant name in the PayPal lightbox | | `shippingAddressOverride` | `Record<string, unknown>` | No | Pre-collected shipping address | | `onCancel` | `(data: BraintreeOnCancelData) => void` | No | Called when buyer cancels | | `onError` | `(err: Error) => void` | No | Called on errors | | `presentationMode` | `BraintreePresentationMode` | No | UI mode: `"auto"`, `"popup"`, `"modal"`, `"redirect"`, etc. | | `returnUrl` | `string` | No | Return URL (required for app-switch modes) | | `cancelUrl` | `string` | No | Cancel URL (required for app-switch modes) | | `type` | `"pay" \| "checkout" \| "buynow" \| "donate" \| "subscribe"` | No | Button label type (default: `"pay"`) | | `disabled` | `boolean` | No | Disable the button | ### BraintreePayPalCheckoutWithVaultButton Renders a `<paypal-button>` for a combined flow — charging the buyer and saving their payment method in a single transaction (one-time payment + billing agreement consent). ```tsx import { BraintreePayPalCheckoutWithVaultButton, useBraintreePayPal, } from "@paypal/react-paypal-js/sdk-v6"; import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6"; function CheckoutWithVaultButton() { const { braintreePayPalCheckoutInstance } = useBraintreePayPal(); return ( <BraintreePayPalCheckoutWithVaultButton amount="49.99" currency="USD" intent="capture" type="pay" billingAgreementDetails={{ description: "Monthly subscription to Products!", }} onApprove={async (data: BraintreeApprovalData) => { const { nonce } = await braintreePayPalCheckoutInstance!.tokenizePayment(data); await fetch("/api/braintree/checkout-and-vault", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nonce }), }); }} onCancel={() => console.log("Cancelled")} onError={(err) => console.error("Error", err)} /> ); } ``` **Props:** | Prop | Type | Required | Description | | ------------------------- | ------------------------------------------------------------- | -------- | ----------------------------------------------------------------- | | `amount` | `string` | Yes | Payment amount (e.g., `"49.99"`) | | `currency` | `string` | Yes | ISO 4217 currency code | | `onApprove` | `(data: BraintreeApprovalData) => Promise<void>` | Yes | Called when buyer approves — tokenize the payment here | | `intent` | `"authorize" \| "capture" \| "order"