Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the getErrorMessage function #190

Merged
merged 22 commits into from Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 11 additions & 21 deletions 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,
Expand Down Expand Up @@ -79,7 +79,14 @@ export const PayPalButtons: FunctionComponent<PayPalButtonsComponentProps> = ({
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;
}
Expand Down Expand Up @@ -170,21 +177,4 @@ export const PayPalButtons: FunctionComponent<PayPalButtonsComponentProps> = ({
);
};

function getErrorMessage({
components = "",
[DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE,
}) {
let errorMessage = `Unable to render <PayPalButtons /> 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\`<PayPalScriptProvider options={{ components: '${expectedComponents}'}}>\`.`;
}

return errorMessage;
}
PayPalButtons.displayName = "PayPalButtons";
34 changes: 12 additions & 22 deletions 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,
Expand Down Expand Up @@ -84,10 +84,16 @@ export const PayPalMarks: FC<PayPalMarksComponentProps> = ({
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 }));
Expand All @@ -105,20 +111,4 @@ export const PayPalMarks: FC<PayPalMarksComponentProps> = ({
);
};

function getErrorMessage({
components = "",
[DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE,
}) {
let errorMessage = `Unable to render <PayPalMarks /> 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\`<PayPalScriptProvider options={{ components: '${expectedComponents}'}}>\`.`;
}

return errorMessage;
}
PayPalMarks.displayName = "PayPalMarks";
37 changes: 13 additions & 24 deletions 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,
Expand Down Expand Up @@ -39,10 +39,17 @@ export const PayPalMessages: FunctionComponent<PayPalMessagesComponentProps> =
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({
Expand Down Expand Up @@ -77,22 +84,4 @@ export const PayPalMessages: FunctionComponent<PayPalMessagesComponentProps> =
return <div ref={messagesContainerRef} className={className} />;
};

function getErrorMessage({
components = "",
[DATA_NAMESPACE]: dataNamespace = DEFAULT_PAYPAL_NAMESPACE,
}) {
let errorMessage = `Unable to render <PayPalMessages /> 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\`<PayPalScriptProvider options={{ components: '${expectedComponents}'}}>\`.`;
}

return errorMessage;
}
PayPalMessages.displayName = "PayPalMessages";
6 changes: 5 additions & 1 deletion src/components/__snapshots__/PayPalButtons.test.tsx.snap
Expand Up @@ -2,7 +2,11 @@

exports[`<PayPalButtons /> should catch and throw unexpected zoid render errors 1`] = `"Failed to render <PayPalButtons /> component. Unknown error"`;

exports[`<PayPalButtons /> should throw an error when no components are passed to the PayPalScriptProvider 1`] = `"Unable to render <PayPalButtons /> because window.paypal.Buttons is undefined."`;
exports[`<PayPalButtons /> should throw an error when no components are passed to the PayPalScriptProvider 1`] = `
"Unable to render <PayPalButtons /> because window.paypal.Buttons is undefined.
To fix the issue, add 'buttons' to the list of components passed to the parent PayPalScriptProvider:
\`<PayPalScriptProvider options={{ components: 'buttons'}}>\`."
`;

exports[`<PayPalButtons /> should throw an error when the 'buttons' component is missing from the components list passed to the PayPalScriptProvider 1`] = `
"Unable to render <PayPalButtons /> because window.paypal.Buttons is undefined.
Expand Down
4 changes: 2 additions & 2 deletions src/components/braintree/BraintreePayPalButtons.tsx
Expand Up @@ -102,10 +102,10 @@ export const BraintreePayPalButtons: FC<BraintreePayPalButtonsComponentProps> =
className={className}
disabled={disabled}
forceReRender={forceReRender}
{...decorateActions(
{...(decorateActions(
buttonProps,
providerContext.braintreePayPalCheckoutInstance
) as PayPalButtonsComponentProps}
) as PayPalButtonsComponentProps)}
>
{children}
</PayPalButtons>
Expand Down
21 changes: 10 additions & 11 deletions src/components/hostedFields/PayPalHostedFieldsProvider.tsx
Expand Up @@ -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";
Expand Down Expand Up @@ -44,20 +44,19 @@ export const PayPalHostedFieldsProvider: FC<PayPalHostedFieldsComponentProps> =
// 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
Expand Down
28 changes: 13 additions & 15 deletions src/components/hostedFields/utils.test.js
Expand Up @@ -2,7 +2,7 @@ import React from "react";

import { PayPalHostedField } from "./PayPalHostedField";
import {
throwMissingHostedFieldsError,
generateMissingHostedFieldsError,
generateHostedFieldsFromChildren,
} from "./utils";
import { DATA_NAMESPACE } from "../../constants";
Expand All @@ -11,34 +11,32 @@ import { PAYPAL_HOSTED_FIELDS_TYPES } from "../../types/enums";
const exceptionMessagePayPalNamespace =
"Unable to render <PayPalHostedFieldsProvider /> because window.paypal.HostedFields is undefined.\nTo fix the issue, add 'hosted-fields' to the list of components passed to the parent PayPalScriptProvider: <PayPalScriptProvider options={{ components: 'hosted-fields'}}>";

describe("throwMissingHostedFieldsError", () => {
describe("generateMissingHostedFieldsError", () => {
const exceptionMessage =
"Unable to render <PayPalHostedFieldsProvider /> because window.Braintree.HostedFields is undefined.\nTo fix the issue, add 'hosted-fields' to the list of components passed to the parent PayPalScriptProvider: <PayPalScriptProvider options={{ components: 'marks,hosted-fields'}}>";

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 <PayPalHostedFieldsProvider /> because window.paypal.HostedFields is undefined."
)
expect(
generateMissingHostedFieldsError({ components: "hosted-fields" })
).toEqual(
"Unable to render <PayPalHostedFieldsProvider /> because window.paypal.HostedFields is undefined."
);
});
});
Expand Down
6 changes: 3 additions & 3 deletions src/components/hostedFields/utils.ts
Expand Up @@ -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";
Expand All @@ -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: <PayPalScriptProvider options={{ components: '${expectedComponents}'}}>`;
}

throw new Error(errorMessage);
return errorMessage;
};

/**
Expand Down
34 changes: 34 additions & 0 deletions src/utils.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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\`<PayPalScriptProvider options={{ components: '${expectedComponents}'}}>\`.`;
}

return errorMessage;
}