@paypal/react-paypal-js
Version:
React components for the PayPal JS SDK
1,076 lines (909 loc) • 99.9 kB
Markdown
# 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"