diff --git a/src/components/PayPalButtons.tsx b/src/components/PayPalButtons.tsx index 74f01815..d7efa451 100644 --- a/src/components/PayPalButtons.tsx +++ b/src/components/PayPalButtons.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FunctionComponent } from "react"; import { usePayPalScriptReducer } from "../hooks/scriptProviderHooks"; -import { getPayPalWindowNamespace } from "../utils"; -import { DEFAULT_PAYPAL_NAMESPACE, DATA_NAMESPACE } from "../constants"; +import { getPayPalWindowNamespace, generateErrorMessage } from "../utils"; +import { DATA_NAMESPACE } from "../constants"; import type { PayPalButtonsComponent, OnInitActions, @@ -79,7 +79,14 @@ export const PayPalButtons: FunctionComponent = ({ paypalWindowNamespace.Buttons === undefined ) { setErrorState(() => { - throw new Error(getErrorMessage(options)); + throw new Error( + generateErrorMessage({ + reactComponentName: PayPalButtons.displayName as string, + sdkComponentKey: "buttons", + sdkRequestedComponents: options.components, + sdkDataNamespace: options[DATA_NAMESPACE], + }) + ); }); return closeButtonsComponent; } @@ -170,21 +177,4 @@ export const PayPalButtons: FunctionComponent = ({ ); }; -function getErrorMessage({ - components = "", - [DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE, -}) { - let errorMessage = `Unable to render because window.${dataNamespace}.Buttons is undefined.`; - - // the JS SDK includes the Buttons component by default when no 'components' are specified. - // The 'buttons' component must be included in the 'components' list when using it with other components. - if (components.length && !components.includes("buttons")) { - const expectedComponents = `${components},buttons`; - - errorMessage += - "\nTo fix the issue, add 'buttons' to the list of components passed to the parent PayPalScriptProvider:" + - `\n\`\`.`; - } - - return errorMessage; -} +PayPalButtons.displayName = "PayPalButtons"; diff --git a/src/components/PayPalMarks.tsx b/src/components/PayPalMarks.tsx index 45e664da..2ccd4eb4 100644 --- a/src/components/PayPalMarks.tsx +++ b/src/components/PayPalMarks.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FC, ReactNode } from "react"; import { usePayPalScriptReducer } from "../hooks/scriptProviderHooks"; -import { getPayPalWindowNamespace } from "../utils"; -import { DEFAULT_PAYPAL_NAMESPACE, DATA_NAMESPACE } from "../constants"; +import { getPayPalWindowNamespace, generateErrorMessage } from "../utils"; +import { DATA_NAMESPACE } from "../constants"; import type { PayPalMarksComponentOptions, PayPalMarksComponent, @@ -84,10 +84,16 @@ export const PayPalMarks: FC = ({ paypalWindowNamespace === undefined || paypalWindowNamespace.Marks === undefined ) { - setErrorState(() => { - throw new Error(getErrorMessage(options)); + return setErrorState(() => { + throw new Error( + generateErrorMessage({ + reactComponentName: PayPalMarks.displayName as string, + sdkComponentKey: "marks", + sdkRequestedComponents: options.components, + sdkDataNamespace: options[DATA_NAMESPACE], + }) + ); }); - return; } renderPayPalMark(paypalWindowNamespace.Marks({ ...markProps })); @@ -105,20 +111,4 @@ export const PayPalMarks: FC = ({ ); }; -function getErrorMessage({ - components = "", - [DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE, -}) { - let errorMessage = `Unable to render because window.${dataNamespace}.Marks is undefined.`; - - // the JS SDK does not load the Marks component by default. It must be passed into the "components" query parameter. - if (!components.includes("marks")) { - const expectedComponents = components ? `${components},marks` : "marks"; - - errorMessage += - "\nTo fix the issue, add 'marks' to the list of components passed to the parent PayPalScriptProvider:" + - `\n\`\`.`; - } - - return errorMessage; -} +PayPalMarks.displayName = "PayPalMarks"; diff --git a/src/components/PayPalMessages.tsx b/src/components/PayPalMessages.tsx index 9d0e36a0..bf678a94 100644 --- a/src/components/PayPalMessages.tsx +++ b/src/components/PayPalMessages.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FunctionComponent } from "react"; import { usePayPalScriptReducer } from "../hooks/scriptProviderHooks"; -import { getPayPalWindowNamespace } from "../utils"; -import { DEFAULT_PAYPAL_NAMESPACE, DATA_NAMESPACE } from "../constants"; +import { getPayPalWindowNamespace, generateErrorMessage } from "../utils"; +import { DATA_NAMESPACE } from "../constants"; import type { PayPalMessagesComponentOptions, PayPalMessagesComponent, @@ -39,10 +39,17 @@ export const PayPalMessages: FunctionComponent = paypalWindowNamespace === undefined || paypalWindowNamespace.Messages === undefined ) { - setErrorState(() => { - throw new Error(getErrorMessage(options)); + return setErrorState(() => { + throw new Error( + generateErrorMessage({ + reactComponentName: + PayPalMessages.displayName as string, + sdkComponentKey: "messages", + sdkRequestedComponents: options.components, + sdkDataNamespace: options[DATA_NAMESPACE], + }) + ); }); - return; } messages.current = paypalWindowNamespace.Messages({ @@ -77,22 +84,4 @@ export const PayPalMessages: FunctionComponent = return
; }; -function getErrorMessage({ - components = "", - [DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE, -}) { - let errorMessage = `Unable to render because window.${dataNamespace}.Messages is undefined.`; - - // the JS SDK does not load the Messages component by default. It must be passed into the "components" query parameter. - if (!components.includes("messages")) { - const expectedComponents = components - ? `${components},messages` - : "messages"; - - errorMessage += - "\nTo fix the issue, add 'messages' to the list of components passed to the parent PayPalScriptProvider:" + - `\n\`\`.`; - } - - return errorMessage; -} +PayPalMessages.displayName = "PayPalMessages"; diff --git a/src/components/__snapshots__/PayPalButtons.test.tsx.snap b/src/components/__snapshots__/PayPalButtons.test.tsx.snap index bf5ed364..c547f663 100644 --- a/src/components/__snapshots__/PayPalButtons.test.tsx.snap +++ b/src/components/__snapshots__/PayPalButtons.test.tsx.snap @@ -2,7 +2,11 @@ exports[` should catch and throw unexpected zoid render errors 1`] = `"Failed to render component. Unknown error"`; -exports[` should throw an error when no components are passed to the PayPalScriptProvider 1`] = `"Unable to render because window.paypal.Buttons is undefined."`; +exports[` should throw an error when no components are passed to the PayPalScriptProvider 1`] = ` +"Unable to render because window.paypal.Buttons is undefined. +To fix the issue, add 'buttons' to the list of components passed to the parent PayPalScriptProvider: +\`\`." +`; exports[` should throw an error when the 'buttons' component is missing from the components list passed to the PayPalScriptProvider 1`] = ` "Unable to render because window.paypal.Buttons is undefined. diff --git a/src/components/braintree/BraintreePayPalButtons.tsx b/src/components/braintree/BraintreePayPalButtons.tsx index d178a8bb..35158983 100644 --- a/src/components/braintree/BraintreePayPalButtons.tsx +++ b/src/components/braintree/BraintreePayPalButtons.tsx @@ -102,10 +102,10 @@ export const BraintreePayPalButtons: FC = className={className} disabled={disabled} forceReRender={forceReRender} - {...decorateActions( + {...(decorateActions( buttonProps, providerContext.braintreePayPalCheckoutInstance - ) as PayPalButtonsComponentProps} + ) as PayPalButtonsComponentProps)} > {children} diff --git a/src/components/hostedFields/PayPalHostedFieldsProvider.tsx b/src/components/hostedFields/PayPalHostedFieldsProvider.tsx index 347fd81c..f2c9a8f7 100644 --- a/src/components/hostedFields/PayPalHostedFieldsProvider.tsx +++ b/src/components/hostedFields/PayPalHostedFieldsProvider.tsx @@ -6,7 +6,7 @@ import { useScriptProviderContext } from "../../hooks/scriptProviderHooks"; import { DATA_NAMESPACE } from "../../constants"; import { generateHostedFieldsFromChildren, - throwMissingHostedFieldsError, + generateMissingHostedFieldsError, } from "./utils"; import { validateHostedFieldChildren } from "./validators"; import { SCRIPT_LOADING_STATE } from "../../types/enums"; @@ -44,20 +44,19 @@ export const PayPalHostedFieldsProvider: FC = // Only render the hosted fields when script is loaded and hostedFields is eligible if (!(loadingStatus === SCRIPT_LOADING_STATE.RESOLVED)) return; // Get the hosted fields from the [window.paypal.HostedFields] SDK - if (!hostedFields.current) { - // Set HostedFields SDK in the mount process only - hostedFields.current = getPayPalWindowNamespace( - options[DATA_NAMESPACE] - ).HostedFields; + hostedFields.current ??= getPayPalWindowNamespace( + options[DATA_NAMESPACE] + ).HostedFields; - if (!hostedFields.current) { - throwMissingHostedFieldsError({ + if (!hostedFields.current) { + throw new Error( + generateMissingHostedFieldsError({ components: options.components, [DATA_NAMESPACE]: options[DATA_NAMESPACE], - }); - } + }) + ); } - if (!hostedFields?.current?.isEligible()) { + if (!hostedFields.current.isEligible()) { return setIsEligible(false); } // Clean all the fields before the rerender diff --git a/src/components/hostedFields/utils.test.js b/src/components/hostedFields/utils.test.js index 50c2b6b7..c0f61363 100644 --- a/src/components/hostedFields/utils.test.js +++ b/src/components/hostedFields/utils.test.js @@ -2,7 +2,7 @@ import React from "react"; import { PayPalHostedField } from "./PayPalHostedField"; import { - throwMissingHostedFieldsError, + generateMissingHostedFieldsError, generateHostedFieldsFromChildren, } from "./utils"; import { DATA_NAMESPACE } from "../../constants"; @@ -11,34 +11,32 @@ import { PAYPAL_HOSTED_FIELDS_TYPES } from "../../types/enums"; const exceptionMessagePayPalNamespace = "Unable to render because window.paypal.HostedFields is undefined.\nTo fix the issue, add 'hosted-fields' to the list of components passed to the parent PayPalScriptProvider: "; -describe("throwMissingHostedFieldsError", () => { +describe("generateMissingHostedFieldsError", () => { const exceptionMessage = "Unable to render because window.Braintree.HostedFields is undefined.\nTo fix the issue, add 'hosted-fields' to the list of components passed to the parent PayPalScriptProvider: "; test("should throw exception with Braintree namespace", () => { - expect(() => { - throwMissingHostedFieldsError({ + expect( + generateMissingHostedFieldsError({ components: "marks", [DATA_NAMESPACE]: "Braintree", - }); - }).toThrow(new Error(exceptionMessage)); + }) + ).toEqual(exceptionMessage); }); test("should throw exception with default namespace", () => { - expect(() => { - throwMissingHostedFieldsError({}); - }).toThrow(new Error(exceptionMessagePayPalNamespace)); + expect(generateMissingHostedFieldsError({})).toEqual( + exceptionMessagePayPalNamespace + ); }); test("should throw exception unknown exception ", () => { window.paypal = {}; - expect(() => { - throwMissingHostedFieldsError({ components: "hosted-fields" }); - }).toThrow( - new Error( - "Unable to render because window.paypal.HostedFields is undefined." - ) + expect( + generateMissingHostedFieldsError({ components: "hosted-fields" }) + ).toEqual( + "Unable to render because window.paypal.HostedFields is undefined." ); }); }); diff --git a/src/components/hostedFields/utils.ts b/src/components/hostedFields/utils.ts index 3f663a1f..585d80bd 100644 --- a/src/components/hostedFields/utils.ts +++ b/src/components/hostedFields/utils.ts @@ -29,10 +29,10 @@ type PayPalHostedFieldOption = { * @throws {@code Error} * */ -export const throwMissingHostedFieldsError = ({ +export const generateMissingHostedFieldsError = ({ components = "", [DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE, -}: PayPalHostedFieldsNamespace): never => { +}: PayPalHostedFieldsNamespace): string => { const expectedComponents = components ? `${components},hosted-fields` : "hosted-fields"; @@ -42,7 +42,7 @@ export const throwMissingHostedFieldsError = ({ errorMessage += `\nTo fix the issue, add 'hosted-fields' to the list of components passed to the parent PayPalScriptProvider: `; } - throw new Error(errorMessage); + return errorMessage; }; /** diff --git a/src/utils.ts b/src/utils.ts index 483fd601..069d52b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,13 @@ import { import type { PayPalNamespace } from "@paypal/paypal-js"; import type { BraintreeNamespace } from "./types"; +type ErrorMessageParams = { + reactComponentName: string; + sdkComponentKey: string; + sdkRequestedComponents?: string; + sdkDataNamespace?: string; +}; + /** * Get the namespace from the window in the browser * this is useful to get the paypal object from window @@ -56,3 +63,30 @@ export function hashStr(str: string): string { return hash; } + +export function generateErrorMessage({ + reactComponentName, + sdkComponentKey, + sdkRequestedComponents = "", + sdkDataNamespace = DEFAULT_PAYPAL_NAMESPACE, +}: ErrorMessageParams): string { + const requiredOptionCapitalized = sdkComponentKey + .charAt(0) + .toUpperCase() + .concat(sdkComponentKey.substring(1)); + let errorMessage = `Unable to render <${reactComponentName} /> because window.${sdkDataNamespace}.${requiredOptionCapitalized} is undefined.`; + + // The JS SDK only loads the buttons component by default. + // All other components like messages and marks must be requested using the "components" query parameter + if (!sdkRequestedComponents.includes(sdkComponentKey)) { + const expectedComponents = [sdkRequestedComponents, sdkComponentKey] + .filter(Boolean) + .join(); + + errorMessage += + `\nTo fix the issue, add '${sdkComponentKey}' to the list of components passed to the parent PayPalScriptProvider:` + + `\n\`\`.`; + } + + return errorMessage; +}