@paypal/react-paypal-js
Version:
React components for the PayPal JS SDK
1,539 lines (1,521 loc) • 109 kB
JavaScript
"use client";
/*!
* react-paypal-js v9.2.0 (2026-04-27T17:34:47.479Z)
* Copyright 2020-present, PayPal, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { createContext, useContext, useRef, useState, useCallback, useEffect, useMemo, useLayoutEffect, useReducer } from 'react';
import { loadCoreSdkScript } from '@paypal/paypal-js/sdk-v6';
var INSTANCE_LOADING_STATE;
(function (INSTANCE_LOADING_STATE) {
INSTANCE_LOADING_STATE["PENDING"] = "pending";
INSTANCE_LOADING_STATE["RESOLVED"] = "resolved";
INSTANCE_LOADING_STATE["REJECTED"] = "rejected";
})(INSTANCE_LOADING_STATE || (INSTANCE_LOADING_STATE = {}));
var INSTANCE_DISPATCH_ACTION;
(function (INSTANCE_DISPATCH_ACTION) {
INSTANCE_DISPATCH_ACTION["SET_LOADING_STATUS"] = "setLoadingStatus";
INSTANCE_DISPATCH_ACTION["SET_INSTANCE"] = "setInstance";
INSTANCE_DISPATCH_ACTION["SET_ELIGIBILITY"] = "setEligibility";
INSTANCE_DISPATCH_ACTION["SET_ERROR"] = "setError";
INSTANCE_DISPATCH_ACTION["RESET_STATE"] = "resetState";
})(INSTANCE_DISPATCH_ACTION || (INSTANCE_DISPATCH_ACTION = {}));
var BRAINTREE_DISPATCH_ACTION;
(function (BRAINTREE_DISPATCH_ACTION) {
BRAINTREE_DISPATCH_ACTION["SET_LOADING_STATUS"] = "setLoadingStatus";
BRAINTREE_DISPATCH_ACTION["SET_INSTANCE"] = "setInstance";
BRAINTREE_DISPATCH_ACTION["SET_ERROR"] = "setError";
BRAINTREE_DISPATCH_ACTION["RESET_STATE"] = "resetState";
})(BRAINTREE_DISPATCH_ACTION || (BRAINTREE_DISPATCH_ACTION = {}));
// ---- Shared types ----
function validateBraintreeNamespace(namespace) {
const ns = namespace;
return typeof ns?.client?.create === "function" && typeof ns?.paypalCheckoutV6?.create === "function";
}
const initialState = {
sdkInstance: null,
eligiblePaymentMethods: null,
eligiblePaymentMethodsPayload: null,
loadingStatus: INSTANCE_LOADING_STATE.PENDING,
error: null,
isHydrated: false
};
function instanceReducer(state, action) {
switch (action.type) {
case INSTANCE_DISPATCH_ACTION.SET_LOADING_STATUS:
return {
...state,
loadingStatus: action.value
};
case INSTANCE_DISPATCH_ACTION.SET_INSTANCE:
return {
...state,
sdkInstance: action.value,
loadingStatus: INSTANCE_LOADING_STATE.RESOLVED
};
case INSTANCE_DISPATCH_ACTION.SET_ELIGIBILITY:
return {
...state,
eligiblePaymentMethods: action.value.eligiblePaymentMethods,
eligiblePaymentMethodsPayload: action.value.payload
};
case INSTANCE_DISPATCH_ACTION.SET_ERROR:
return {
...state,
error: action.value,
loadingStatus: INSTANCE_LOADING_STATE.REJECTED
};
case INSTANCE_DISPATCH_ACTION.RESET_STATE:
return initialState;
default:
return state;
}
}
const PayPalContext = createContext(null);
/**
* Returns {@link PayPalState} provided by a parent {@link PayPalProvider}.
*
* @returns {PayPalState}
*/
function usePayPal() {
const context = useContext(PayPalContext);
if (context === null) {
throw new Error("usePayPal must be used within a PayPalProvider");
}
return context;
}
/**
* Performs a shallow equality check on two arrays.
*
* This function compares two arrays element-by-element using strict equality (===).
* It's primarily used to compare the `components` prop arrays passed to PayPalProvider
* to prevent unnecessary re-initialization of the PayPal SDK.
*
* This optimization is important because re-initializing the SDK is an expensive operation
* that involves loading scripts and setting up PayPal integrations.
*
* @param arr1 - First array to compare
* @param arr2 - Second array to compare
* @returns `true` if both arrays are null/undefined, or if they contain the same elements in the same order
*
* @example
* // Returns true - both arrays have the same elements in the same order
* shallowEqualArray(
* ["paypal-payments", "venmo-payments"],
* ["paypal-payments", "venmo-payments"]
* );
*
* @example
* // Returns false - different order
* shallowEqualArray(
* ["paypal-payments", "venmo-payments"],
* ["venmo-payments", "paypal-payments"]
* );
*
* @example
* // Returns true - both are null
* shallowEqualArray(null, null);
*/
function shallowEqualArray(arr1, arr2) {
if (!arr1 && !arr2) {
return true;
}
if (!arr1 || !arr2) {
return false;
}
if (arr1.length !== arr2.length) {
return false;
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
/**
* Custom hook that memoizes a components array based on shallow equality comparison.
* Returns a stable reference when the array contents haven't changed.
*
* This allows developers to pass inline component arrays without causing unnecessary re-renders
* when the array values are the same, even if the array reference changes.
*
* @param value - The components array to memoize
* @returns A stable reference to the components array
*
* @example
* const memoizedComponents = useCompareMemoize(["paypal-payments", "venmo-payments"]);
*/
function useCompareMemoize(value) {
const ref = useRef(value);
if (!shallowEqualArray(ref.current, value)) {
ref.current = value;
}
return ref.current;
}
function useProxyProps(props) {
const proxyRef = useRef(new Proxy({}, {
get(target, prop, receiver) {
/**
*
* If target[prop] is a function, return a function that accesses
* this function off the target object. We can mutate the target with
* new copies of this function without having to re-render the
* SDK components to pass new callbacks.
*
* */
if (typeof target[prop] === "function") {
return (...args) => target[prop](...args);
}
return Reflect.get(target, prop, receiver);
}
}));
proxyRef.current = Object.assign(proxyRef.current, props);
return proxyRef.current;
}
/**
* Normalize input to an {@link Error} instance.
*
* @param {unknown} error - this argument will be coerced into a String then passed into a new
* {@link Error}. If it's already an {@link Error} instance, it will be returned without modification.
* @returns {Error}
*
* @example
* toError("An error occurred");
*
* @example
* const myError = new Error("An error occurred");
* toError(myError);
*/
function toError(error) {
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
/**
* Custom hook that memoizes a value based on deep equality comparison.
* Returns a stable reference when the value hasn't changed, even if the
* object or array reference is new.
*
* This allows developers to pass inline objects or arrays without causing
* unnecessary re-renders or effect re-runs when the values are the same.
*
* @param value - The value to memoize
* @returns A stable reference to the value
*
* @example
* const memoizedAmount = useDeepCompareMemoize({ value: "10.00", currencyCode: "USD" });
*/
function useDeepCompareMemoize(value) {
const ref = useRef();
if (!ref.current || !deepEqual(value, ref.current.value)) {
ref.current = {
value
};
}
return ref.current.value;
}
/**
* Performs a recursive deep equality check on two values.
*
* Handles primitives, null/undefined, arrays, and plain objects. Recursion is
* bounded by `maxDepth` (default: 10) to prevent stack overflow on deeply nested
* structures — comparison returns `false` if the limit is exceeded.
*
* @param obj1 - First value to compare
* @param obj2 - Second value to compare
* @param maxDepth - Maximum recursion depth (default: 10)
* @param currentDepth - Current recursion depth, used internally
* @returns `true` if both values are deeply equal, `false` otherwise
*
* @example
* deepEqual({ amount: "10.00", currency: "USD" }, { amount: "10.00", currency: "USD" }); // true
* deepEqual({ amount: "10.00" }, { amount: "20.00" }); // false
*/
// eslint-disable-next-line max-params
function deepEqual(obj1, obj2, maxDepth = 10, currentDepth = 0) {
// Prevent infinite recursion by limiting depth
if (currentDepth > maxDepth) {
return false;
}
// Handle primitives and same reference
if (obj1 === obj2) {
return true;
}
// Handle null/undefined
if (obj1 === null || obj1 === undefined || obj2 === null || obj2 === undefined) {
return false;
}
// Different types are not equal
if (typeof obj1 !== typeof obj2) {
return false;
}
// Non-object primitives (number, string, boolean, function, symbol, bigint) that
// Same typeof (verified above) but not ===, so unequal primitives
if (typeof obj1 !== "object") {
return false;
}
// Handle Arrays
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) {
return false;
}
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i], maxDepth, currentDepth + 1)) {
return false;
}
}
return true;
}
// One is array, the other is not
if (Array.isArray(obj1) || Array.isArray(obj2)) {
return false;
}
// At this point, we know both are non-null objects
const record1 = obj1;
const record2 = obj2;
const keys1 = Object.keys(record1);
const keys2 = Object.keys(record2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!deepEqual(record1[key], record2[key], maxDepth, currentDepth + 1)) {
return false;
}
}
return true;
}
/**
* Creates a payment session with error handling and retry prevention.
*
* @param options - Configuration for creating the payment session
* @returns The payment session or null if creation fails
*
* @example
* const session = createPaymentSession({
* sessionCreator: () => sdkInstance.createPayPalOneTimePaymentSession({ orderId, ...callbacks }),
* failedSdkRef,
* sdkInstance,
* setError,
* errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-payments" is not included in the SDK components array.',
* });
*
* if (!session) return;
*/
function createPaymentSession({
sessionCreator,
failedSdkRef,
sdkInstance,
setError,
errorMessage
}) {
// Skip retry if this SDK instance already failed
if (failedSdkRef.current === sdkInstance) {
return null;
}
try {
return sessionCreator();
} catch (err) {
failedSdkRef.current = sdkInstance;
const detailedError = new Error(errorMessage, {
cause: err
});
setError(detailedError);
return null;
}
}
const CardFieldsSessionContext = createContext(null);
const CardFieldsStatusContext = createContext(null);
/**
* Centralized hook for handling {@link Error}s in a consistent manner.
*
* @param {Boolean} noConsoleErrors - set to `true` to prevent `setError` calls from logging to `console.error`.
*/
function useError(noConsoleErrors = false) {
const [error, setErrorInternal] = useState(null);
const setError = useCallback(newError => {
setErrorInternal(newError);
if (!noConsoleErrors && newError) {
console.error(newError);
}
}, [noConsoleErrors]);
return [error, setError];
}
const CARD_FIELDS_SESSION_TYPES = {
ONE_TIME_PAYMENT: "one-time-payment",
SAVE_PAYMENT: "save-payment"
};
/**
* {@link PayPalCardFieldsProvider} creates a Card Fields session and provides it to child components.
*
* @remarks
* Child components must use either {@link usePayPalCardFieldsOneTimePaymentSession} or
* {@link usePayPalCardFieldsSavePaymentSession} to initialize the appropriate session type.
* The session will not be created until one of these hooks is called.
*
* @example
* // Amount can be updated dynamically
* const [amount, setAmount] = useState<OrderAmount>({ value: "10.00", currencyCode: "USD" });
* const onBlur = useCallback((event) => { ... }, []);
* <PayPalProvider
* components={["card-fields"]}
* clientToken={clientToken}
* pageType="checkout"
* >
* <PayPalCardFieldsProvider
* blur={onBlur}
* validitychange={(event) => console.log('Validity:', event)}
* cardtypechange={(event) => console.log('Card type:', event)}
* amount={amount}
* isCobrandedEligible={true}
* >
* <CheckoutForm />
* </PayPalCardFieldsProvider>
* </PayPalProvider>
*/
const PayPalCardFieldsProvider = ({
children,
amount,
isCobrandedEligible,
...eventHandlers
}) => {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const [cardFieldsSession, setCardFieldsSession] = useState(null);
const [cardFieldsSessionType, setCardFieldsSessionType] = useState(null);
const [cardFieldsError, setCardFieldsError] = useState(null);
const [, setError] = useError();
// Use proxy props for event handlers to avoid re-renders
const proxyEventHandlers = useProxyProps(eventHandlers);
// Memoize amount to avoid unnecessary updates when value hasn't changed
const memoizedAmount = useDeepCompareMemoize(amount);
const handleError = useCallback(error => {
setError(error);
setCardFieldsError(error);
}, [setError]);
// Effect to create Card Fields session
useEffect(() => {
// Early return: Still loading, wait for sdkInstance
if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) {
return;
}
// Error case: Loading finished but no sdkInstance
if (!sdkInstance) {
handleError(toError("no sdk instance available"));
return;
}
// Clear previous sdkInstance loading errors
handleError(null);
// Wait for session type to be set by child hooks
if (!cardFieldsSessionType) {
return;
}
// Create Card Fields session based on sessionType
let newCardFieldsSession;
try {
newCardFieldsSession = cardFieldsSessionType === CARD_FIELDS_SESSION_TYPES.ONE_TIME_PAYMENT ? sdkInstance.createCardFieldsOneTimePaymentSession() : sdkInstance.createCardFieldsSavePaymentSession();
setCardFieldsSession(newCardFieldsSession);
} catch (error) {
handleError(toError(error));
}
// Cleanup: destroy session on unmount or when dependencies change
return () => {
newCardFieldsSession?.destroy();
setCardFieldsSession(null);
};
}, [sdkInstance, loadingStatus, cardFieldsSessionType, handleError]);
/**
* Registers Card Fields event handlers with the session.
* Uses useProxyProps for stable handler references and useDeepCompareMemoize to avoid
* unnecessary re-registrations when handler values have not changed.
*
* @remarks
* For best performance, wrap handler functions with useCallback or useDeepCompareMemoize
* before passing them as props to avoid unnecessary SDK event handler re-registrations.
*/
useEffect(() => {
if (!cardFieldsSession) {
return;
}
try {
/*
Register all event handlers that are defined
by iterating over the keys of proxyEventHandlers directly
*/
Object.keys(proxyEventHandlers).forEach(eventName => {
const handler = proxyEventHandlers[eventName];
if (handler && typeof handler === "function") {
cardFieldsSession.on(eventName, handler);
}
});
} catch (error) {
handleError(toError(`Failed to register event handlers: ${error}`));
}
}, [cardFieldsSession, proxyEventHandlers, handleError]);
// Update session configuration when props change
useEffect(() => {
if (!cardFieldsSession) {
return;
}
// Build update configuration from props
const updateOptions = {};
let hasUpdates = false;
if (memoizedAmount !== undefined) {
updateOptions.amount = memoizedAmount;
hasUpdates = true;
}
if (isCobrandedEligible !== undefined) {
updateOptions.isCobrandedEligible = isCobrandedEligible;
hasUpdates = true;
}
// Only call update if there are configuration changes
if (!hasUpdates) {
return;
}
try {
cardFieldsSession.update(updateOptions);
} catch (error) {
handleError(toError(`Failed to update card fields configuration: ${error}`));
}
}, [cardFieldsSession, memoizedAmount, isCobrandedEligible, handleError]);
const sessionContextValue = useMemo(() => ({
cardFieldsSession,
setCardFieldsSessionType,
setError: handleError
}), [cardFieldsSession, setCardFieldsSessionType, handleError]);
const statusContextValue = useMemo(() => ({
error: cardFieldsError
}), [cardFieldsError]);
return React.createElement(CardFieldsSessionContext.Provider, {
value: sessionContextValue
}, React.createElement(CardFieldsStatusContext.Provider, {
value: statusContextValue
}, children));
};
/**
* Return a {@link React.MutableRefObject} a stable ref that's `true` if the component is mounted, `false` otherwise.
*
* The return must, unfortunately be included in dependency arrays. See the issue here: [\[eslint-plugin-react-hooks\] allow configuring custom hooks as "static" #16873](https://github.com/facebook/react/issues/16873).
*/
function useIsMountedRef() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
/**
* Hook for managing Pay Later one-time payment sessions.
*
* This hook creates and manages a Pay Later payment session. It handles session lifecycle, resume flows
* for redirect-based presentation modes (`"redirect"` and `"direct-app-switch"`), and provides methods
* to start, cancel, and destroy the session.
*
* @returns Object with: `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function PayLaterCheckoutButton() {
* const { error, isPending, handleClick, handleCancel } = usePayLaterOneTimePaymentSession({
* presentationMode: 'popup',
* createOrder: async () => ({ orderId: 'ORDER-123' }),
* onApprove: (data) => console.log('Approved:', data),
* onCancel: () => console.log('Cancelled'),
* });
* const { eligiblePaymentMethods } = usePayPal();
* const payLaterDetails = eligiblePaymentMethods?.getDetails?.("paylater");
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-pay-later-button
* countryCode={payLaterDetails?.countryCode}
* productCode={payLaterDetails?.productCode}
* onClick={handleClick}
* onCancel={handleCancel}
* />
* );
* }
*/
function usePayLaterOneTimePaymentSession({
presentationMode,
fullPageOverlay,
autoRedirect,
createOrder,
orderId,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayLaterOneTimePaymentSession({
orderId,
...proxyCallbacks
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-payments" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
// check for resume flow in redirect-based presentation modes
const isRedirectMode = presentationMode === "redirect" || presentationMode === "direct-app-switch";
if (isRedirectMode) {
const handleReturnFromPayPal = async () => {
try {
if (!newSession) {
return;
}
const isResumeFlow = newSession.hasReturned?.();
if (isResumeFlow) {
await newSession.resume?.();
}
} catch (err) {
setError(err);
}
};
handleReturnFromPayPal();
}
return () => {
newSession.destroy();
};
}, [sdkInstance, orderId, proxyCallbacks, presentationMode, setError]);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {
setError(new Error("PayLater session not available"));
return;
}
const startOptions = {
presentationMode,
fullPageOverlay,
autoRedirect
};
const result = await sessionRef.current.start(startOptions, createOrder?.());
return result;
}, [createOrder, presentationMode, fullPageOverlay, autoRedirect, isMountedRef, setError]);
return {
error,
isPending,
handleCancel,
handleClick,
handleDestroy
};
}
/**
* `PayLaterOneTimePaymentButton` is a button that provides a PayLater payment flow.
*
* `PayLaterOneTimePaymentButtonProps` combines the arguments for {@link UsePayLaterOneTimePaymentSessionProps}
* with a `disabled` prop.
*
* The `countryCode` and `productCode` are automatically populated from the eligibility API response
* (available via `usePayPal().eligiblePaymentMethods`). The button requires eligibility to be configured
* in the parent `PayPalProvider`.
*
* Note, `autoRedirect` is not allowed because if given a `presentationMode` of `"redirect"` the button
* would not be able to provide back `redirectURL` from `start`. Advanced integrations that need
* `redirectURL` should use the {@link usePayLaterOneTimePaymentSession} hook directly.
*
* @example
* <PayLaterOneTimePaymentButton
* onApprove={() => {
* // ... on approve logic
* }}
* orderId="your-order-id"
* presentationMode="auto"
* />
*/
const PayLaterOneTimePaymentButton = ({
disabled = false,
...hookProps
}) => {
const {
eligiblePaymentMethods,
isHydrated
} = usePayPal();
const {
error,
isPending,
handleClick
} = usePayLaterOneTimePaymentSession(hookProps);
const payLaterDetails = eligiblePaymentMethods?.getDetails("paylater");
const countryCode = payLaterDetails?.countryCode;
const productCode = payLaterDetails?.productCode;
useEffect(() => {
if (error) {
console.error(error);
}
}, [error]);
if (isPending) {
return null;
}
return isHydrated ? React.createElement("paypal-pay-later-button", {
onClick: handleClick,
countryCode: countryCode,
productCode: productCode,
disabled: disabled || !!error || undefined
}) : React.createElement("div", null);
};
/**
* Hook for managing PayPal Credit one-time payment sessions.
*
* This hook creates and manages a PayPal Credit payment session. It handles session lifecycle, resume flows
* for redirect-based flows, and provides methods to start, cancel, and destroy the session.
*
* @returns Object with: `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function CreditCheckoutButton() {
* const { error, isPending, handleClick, handleCancel } = usePayPalCreditOneTimePaymentSession({
* presentationMode: 'popup',
* createOrder: async () => ({ orderId: 'ORDER-123' }),
* onApprove: (data) => console.log('Approved:', data),
* onCancel: () => console.log('Cancelled'),
* });
* const { eligiblePaymentMethods } = usePayPal();
* const creditDetails = eligiblePaymentMethods?.getDetails?.("credit");
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-credit-button
* countryCode={creditDetails?.countryCode}
* onClick={handleClick}
* onCancel={handleCancel}
* />
* );
* }
*/
function usePayPalCreditOneTimePaymentSession({
presentationMode,
fullPageOverlay,
autoRedirect,
createOrder,
orderId,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayPalCreditOneTimePaymentSession({
orderId,
...proxyCallbacks
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-payments" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
// Only check for resume flow in redirect-based presentation modes
const shouldCheckResume = presentationMode === "redirect" || presentationMode === "direct-app-switch";
if (shouldCheckResume) {
const handleReturnFromPayPal = async () => {
try {
if (!newSession) {
return;
}
const isResumeFlow = newSession.hasReturned?.();
if (isResumeFlow) {
await newSession.resume?.();
}
} catch (err) {
setError(err);
}
};
handleReturnFromPayPal();
}
return () => {
newSession.destroy();
};
}, [sdkInstance, orderId, proxyCallbacks, presentationMode, setError]);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {
setError(new Error("PayPal session not available"));
return;
}
const startOptions = {
presentationMode,
fullPageOverlay,
autoRedirect
};
const result = await sessionRef.current.start(startOptions, createOrder?.());
return result;
}, [isMountedRef, presentationMode, fullPageOverlay, autoRedirect, createOrder, setError]);
return {
error,
isPending,
handleClick,
handleDestroy,
handleCancel
};
}
/**
* `PayPalCreditOneTimePaymentButton` is a button that provides a PayPal Credit payment flow.
*
* The `countryCode` is automatically populated from the eligibility API response
* (available via `usePayPal().eligiblePaymentMethods`). The button requires eligibility to be configured
* in the parent `PayPalProvider`, using either the `useEligibleMethods` hook client-side or `useFetchEligibleMethods` server-side.
*
* Note, `autoRedirect` is not allowed because if given a `presentationMode` of `"redirect"` the button
* would not be able to provide back `redirectURL` from `start`. Advanced integrations that need
* `redirectURL` should use the {@link usePayPalCreditOneTimePaymentSession} hook directly.
*
* @example
* <PayPalCreditOneTimePaymentButton
* onApprove={() => {
* // ... on approve logic
* }}
* orderId="your-order-id"
* presentationMode="auto"
* />
*/
const PayPalCreditOneTimePaymentButton = ({
disabled = false,
...hookProps
}) => {
const {
eligiblePaymentMethods,
isHydrated
} = usePayPal();
const {
error,
isPending,
handleClick
} = usePayPalCreditOneTimePaymentSession(hookProps);
const creditDetails = eligiblePaymentMethods?.getDetails("credit");
const countryCode = creditDetails?.countryCode;
useEffect(() => {
if (error) {
console.error(error);
}
}, [error]);
if (isPending) {
return null;
}
return isHydrated ? React.createElement("paypal-credit-button", {
onClick: handleClick,
countryCode: countryCode,
disabled: disabled || !!error || undefined
}) : React.createElement("div", null);
};
/**
* Hook for managing PayPal Credit save payment sessions.
*
* This hook creates and manages a PayPal Credit save payment session for vaulting payment methods.
* It handles session lifecycle, resume flows for redirect-based flows, and provides methods to start, cancel, and destroy the session.
*
* @returns Object with: `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function SaveCreditButton() {
* const { error, isPending, handleClick, handleCancel } = usePayPalCreditSavePaymentSession({
* presentationMode: 'redirect',
* createVaultToken: async () => ({ vaultSetupToken: 'VAULT-TOKEN-123' }),
* onApprove: (data) => console.log('Vaulted:', data),
* onCancel: () => console.log('Cancelled'),
* });
* const { eligiblePaymentMethods } = usePayPal();
* const creditDetails = eligiblePaymentMethods?.getDetails?.("credit");
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-credit-button
* countryCode={creditDetails?.countryCode}
* onClick={handleClick}
* onCancel={handleCancel}
* />
* );
* }
*/
function usePayPalCreditSavePaymentSession({
presentationMode,
fullPageOverlay,
autoRedirect,
createVaultToken,
vaultSetupToken,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayPalSavePaymentSession({
vaultSetupToken,
...proxyCallbacks
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-payments" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
const shouldCheckResume = presentationMode === "redirect" || presentationMode === "direct-app-switch";
if (shouldCheckResume) {
const handleReturnFromPayPal = async () => {
try {
if (!newSession) {
return;
}
const isResumeFlow = newSession.hasReturned?.();
if (isResumeFlow) {
await newSession.resume?.();
}
} catch (err) {
setError(err);
}
};
handleReturnFromPayPal();
}
return () => {
newSession.destroy();
};
}, [sdkInstance, vaultSetupToken, proxyCallbacks, presentationMode, setError]);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {
setError(new Error("Credit Save Payment session not available"));
return;
}
const startOptions = {
presentationMode,
fullPageOverlay,
autoRedirect
};
if (createVaultToken) {
await sessionRef.current.start(startOptions, createVaultToken());
} else {
await sessionRef.current.start(startOptions);
}
}, [isMountedRef, presentationMode, fullPageOverlay, autoRedirect, createVaultToken, setError]);
return {
error,
isPending,
handleClick,
handleDestroy,
handleCancel
};
}
/**
* `PayPalCreditSavePaymentButton` is a button that provides a PayPal Credit save payment flow.
*
* The `countryCode` is automatically populated from the eligibility API response
* (available via `usePayPal().eligiblePaymentMethods`). The button requires eligibility to be configured
* in the parent `PayPalProvider`, using either the `useEligibleMethods` hook client-side or `useFetchEligibleMethods` server-side.
*
* Note, `autoRedirect` is not allowed because if given a `presentationMode` of `"redirect"` the button
* would not be able to provide back `redirectURL` from `start`. Advanced integrations that need
* `redirectURL` should use the {@link usePayPalCreditSavePaymentSession} hook directly.
*
* @example
* <PayPalCreditSavePaymentButton
* onApprove={() => {
* // ... on approve logic
* }}
* vaultSetupToken="your-vault-setup-token"
* presentationMode="auto"
* />
*/
const PayPalCreditSavePaymentButton = ({
disabled = false,
...hookProps
}) => {
const {
eligiblePaymentMethods,
isHydrated
} = usePayPal();
const {
error,
isPending,
handleClick
} = usePayPalCreditSavePaymentSession(hookProps);
const creditDetails = eligiblePaymentMethods?.getDetails("credit");
const countryCode = creditDetails?.countryCode;
useEffect(() => {
if (error) {
console.error(error);
}
}, [error]);
if (isPending) {
return null;
}
return isHydrated ? React.createElement("paypal-credit-button", {
onClick: handleClick,
countryCode: countryCode,
disabled: disabled || !!error || undefined
}) : React.createElement("div", null);
};
/**
* `usePayPalGuestPaymentSession` is used to interface with a guest checkout session. Guest checkout
* sessions require a `<paypal-basic-card-button>` to target for displaying the guest checkout form.
*
* @returns Object with: `buttonRef` (ref for the target button element), `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function GuestCheckoutButton() {
* const { buttonRef, error, isPending, handleClick, handleCancel } =
* usePayPalGuestPaymentSession({
* createOrder: async () => ({ orderId: 'ORDER-123' }),
* onApprove: (data) => console.log('Approved:', data),
* onCancel: () => console.log('Cancelled'),
* });
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-basic-card-container>
* <paypal-basic-card-button
* onClick={handleClick}
* onCancel={handleCancel}
* ref={buttonRef}
* />
* </paypal-basic-card-container>
* );
* }
*/
function usePayPalGuestPaymentSession({
fullPageOverlay,
createOrder,
orderId,
onShippingAddressChange,
onShippingOptionsChange,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const buttonRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayPalGuestOneTimePaymentSession({
orderId,
...proxyCallbacks,
...(onShippingAddressChange && {
onShippingAddressChange
}),
...(onShippingOptionsChange && {
onShippingOptionsChange
})
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-guest-payments" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
return () => {
newSession.destroy();
};
}, [sdkInstance, orderId, proxyCallbacks, onShippingAddressChange, onShippingOptionsChange, isMountedRef, setError]);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {
setError(new Error("PayPal Guest Checkout session not available"));
return;
}
try {
const startOptions = {
presentationMode: "auto",
fullPageOverlay,
...(buttonRef.current ? {
targetElement: buttonRef.current
} : {})
};
await sessionRef.current.start(startOptions, createOrder?.());
} catch (err) {
if (isMountedRef.current) {
setError(err);
}
}
}, [isMountedRef, fullPageOverlay, createOrder, setError]);
return {
buttonRef,
error,
isPending,
handleClick,
handleCancel,
handleDestroy
};
}
/**
* `PayPalGuestPaymentButton` is a button that provides a guest checkout (BCDC) payment flow.
*
* `PayPalGuestPaymentButtonProps` combines the arguments for {@link UsePayPalGuestPaymentSessionProps}
* with a `disabled` prop.
*
* This component automatically wraps the button with `<paypal-basic-card-container>` which is
* required for the guest checkout form to display properly.
*
* @example
* <PayPalGuestPaymentButton
* createOrder={createOrder}
* onApprove={() => {
* // ... on approve logic
* }}
* />
*/
const PayPalGuestPaymentButton = ({
disabled = false,
...hookProps
}) => {
const {
error,
isPending,
handleClick,
buttonRef
} = usePayPalGuestPaymentSession(hookProps);
const {
isHydrated
} = usePayPal();
useEffect(() => {
if (error) {
console.error(error);
}
}, [error]);
const button = isHydrated ? React.createElement("paypal-basic-card-button", {
ref: buttonRef,
onClick: handleClick,
disabled: disabled || isPending || error !== null ? true : undefined
}) : React.createElement("div", null);
return React.createElement("paypal-basic-card-container", null, button);
};
/**
* Hook for managing one-time payment sessions with PayPal.
*
* The hook returns an `isPending` flag that indicates whether the SDK instance is still being
* initialized. This is useful when using deferred clientToken loading - buttons should wait
* to render until `isPending` is false.
*
* @returns Object with: `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function PayPalCheckoutButton() {
* const { isPending, error, handleClick, handleCancel } = usePayPalOneTimePaymentSession({
* orderId: "ORDER-123",
* presentationMode: "auto",
* onApprove: (data) => console.log("Approved:", data),
* });
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-button onClick={handleClick} onCancel={handleCancel} />
* );
* }
*/
function usePayPalOneTimePaymentSession({
presentationMode,
fullPageOverlay,
autoRedirect,
createOrder,
orderId,
savePayment,
testBuyerCountry,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayPalOneTimePaymentSession({
orderId,
savePayment,
testBuyerCountry,
...proxyCallbacks
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-payments" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
// Only check for resume flow in redirect-based presentation modes
const shouldCheckResume = presentationMode === "redirect" || presentationMode === "direct-app-switch";
if (shouldCheckResume) {
const handleReturnFromPayPal = async () => {
try {
if (!newSession) {
return;
}
const isResumeFlow = newSession.hasReturned?.();
if (isResumeFlow) {
await newSession.resume?.();
}
} catch (err) {
setError(err);
}
};
handleReturnFromPayPal();
}
return () => {
newSession.destroy();
};
}, [sdkInstance, orderId, proxyCallbacks, presentationMode, setError, savePayment, testBuyerCountry]);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {
setError(new Error("PayPal session not available"));
return;
}
const startOptions = {
presentationMode,
fullPageOverlay,
autoRedirect
};
const result = await sessionRef.current.start(startOptions, createOrder?.());
return result;
}, [isMountedRef, presentationMode, fullPageOverlay, autoRedirect, createOrder, setError]);
return {
error,
isPending,
handleClick,
handleCancel,
handleDestroy
};
}
/**
* `PayPalOneTimePaymentButton` is a button that provides a standard PayPal payment flow.
*
* `PayPalOneTimePaymentButtonProps` combines the arguments for {@link UsePayPalOneTimePaymentSessionProps}
* and {@link ButtonProps}.
*
* Note, `autoRedirect` is not allowed because if given a `presentationMode` of `"redirect"` the button
* would not be able to provide back `redirectURL` from `start`. Advanced integrations that need
* `redirectURL` should use the {@link usePayPalOneTimePaymentSession} hook directly.
*
* @example
* <PayPalOneTimePaymentButton
* onApprove={() => {
* // ... on approve logic
* }}
* orderId="your-order-id"
* presentationMode="auto"
* />
*/
const PayPalOneTimePaymentButton = ({
type = "pay",
disabled = false,
...hookProps
}) => {
const {
error,
isPending,
handleClick
} = usePayPalOneTimePaymentSession(hookProps);
const {
isHydrated
} = usePayPal();
useEffect(() => {
if (error) {
console.error(error);
}
}, [error]);
return isHydrated ? React.createElement("paypal-button", {
onClick: handleClick,
type: type,
disabled: disabled || isPending || error !== null ? true : undefined
}) : React.createElement("div", null);
};
/**
* Hook for managing PayPal subscription payment sessions.
*
* This hook creates and manages a PayPal subscription payment session, supporting multiple presentation modes
* including popup and modal. It handles session lifecycle and provides methods to start, cancel, and destroy the session.
*
* @param props - Configuration options including presentation mode and callbacks
* @param props.createSubscription - Function that returns a promise resolving to an object with subscriptionId
* @param props.presentationMode - How the subscription experience is presented: 'popup', 'modal', 'auto', or 'payment-handler'
* @param props.fullPageOverlay - Whether to show a full-page overlay during the subscription flow
* @returns Object with: `error` (any session error), `isPending` (SDK loading), `handleClick` (starts session), `handleCancel` (cancels session), `handleDestroy` (cleanup)
*
* @example
* function SubscriptionCheckoutButton() {
* const { error, isPending, handleClick, handleCancel } = usePayPalSubscriptionPaymentSession({
* presentationMode: 'popup',
* createSubscription: async () => ({ subscriptionId: 'SUB-123' }),
* onApprove: (data) => console.log('Subscription approved:', data),
* onCancel: () => console.log('Subscription cancelled'),
* onError: (err) => console.error('Subscription error:', err),
* });
*
* if (isPending) return null;
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <paypal-button onClick={handleClick} onCancel={handleCancel} />
* );
* }
*/
function usePayPalSubscriptionPaymentSession({
presentationMode,
fullPageOverlay,
createSubscription,
...callbacks
}) {
const {
sdkInstance,
loadingStatus
} = usePayPal();
const isMountedRef = useIsMountedRef();
const sessionRef = useRef(null);
const proxyCallbacks = useProxyProps(callbacks);
const [error, setError] = useError();
// Prevents retrying session creation with a failed SDK instance
const failedSdkRef = useRef(null);
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const handleDestroy = useCallback(() => {
sessionRef.current?.destroy();
sessionRef.current = null;
}, []);
const handleCancel = useCallback(() => {
sessionRef.current?.cancel();
}, []);
// Handle SDK availability
useEffect(() => {
// Reset failed SDK tracking when SDK instance changes
if (failedSdkRef.current !== sdkInstance) {
failedSdkRef.current = null;
}
if (sdkInstance) {
setError(null);
} else if (loadingStatus !== INSTANCE_LOADING_STATE.PENDING) {
setError(new Error("no sdk instance available"));
}
}, [sdkInstance, setError, loadingStatus]);
// Create and manage session lifecycle
useEffect(() => {
if (!sdkInstance) {
return;
}
const newSession = createPaymentSession({
sessionCreator: () => sdkInstance.createPayPalSubscriptionPaymentSession({
...proxyCallbacks
}),
failedSdkRef,
sdkInstance,
setError,
errorMessage: 'Failed to create payment session. This may occur if the required component "paypal-subscriptions" is not included in the SDK components array.'
});
if (!newSession) {
return;
}
sessionRef.current = newSession;
return () => {
newSession.destroy();
};
}, [sdkInstance, proxyCallbacks, setError]);
const handleClick = useCallback(async () => {
if (!isMountedRef.current) {
return;
}
if (!sessionRef.current) {