Skip to content

Commit

Permalink
Merge branch 'main' into feature/update-createOrder
Browse files Browse the repository at this point in the history
  • Loading branch information
ravishekhar committed Jan 17, 2025
2 parents 5748656 + cff6d0f commit 37ef35d
Show file tree
Hide file tree
Showing 17 changed files with 630 additions and 150 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: ▶️ Run Playwright tests
run: npm run test:e2e

- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
Expand Down
496 changes: 370 additions & 126 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/react-paypal-js/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
typescript: {
check: false,
checkOptions: {},
reactDocgen: "react-docgen-typescript",
reactDocgen: "react-docgen-typescript-plugin",
reactDocgenTypescriptOptions: {
// the Storybook docs need this to render the props table for <PayPalButtons />
compilerOptions: {
Expand Down
12 changes: 12 additions & 0 deletions packages/react-paypal-js/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 8.8.1

### Patch Changes

- 10775ad: minor fixes to release and adding react 19 as peer dependency

## 8.8.0

### Minor Changes

- 471280d: (fix) proxy props to prevent stale closure

## 8.7.0

### Minor Changes
Expand Down
1 change: 1 addition & 0 deletions packages/react-paypal-js/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
17 changes: 12 additions & 5 deletions packages/react-paypal-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@paypal/react-paypal-js",
"version": "8.7.0",
"version": "8.8.1",
"description": "React components for the PayPal JS SDK",
"keywords": [
"react",
Expand Down Expand Up @@ -66,8 +66,10 @@
"@storybook/addon-links": "^6.4.9",
"@storybook/react": "^6.4.9",
"@storybook/storybook-deployer": "^2.8.10",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^27.4.0",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
Expand All @@ -84,27 +86,32 @@
"husky": "^7.0.4",
"jest": "^27.5.1",
"jest-mock-extended": "^2.0.4",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "^17.0.2",
"react-docgen-typescript-plugin": "^1.0.8",
"react-dom": "^17.0.2",
"react-element-to-jsx-string": "^14.3.4",
"react-error-boundary": "^3.1.4",
"react-is": "^17.0.2",
"rimraf": "^3.0.2",
"rollup": "^2.67.3",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-terser": "^7.0.2",
"scheduler": "^0.20.2",
"semver": "^7.3.5",
"standard-version": "^9.3.2",
"string-template": "^1.0.0",
"typescript": "^4.7.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17 || ^18 || ^19",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
},
"jest": {
"transformIgnorePatterns": [
"/!node_modules\\/@paypal\\/sdk-constants/"
],
"setupFilesAfterEnv": [
"./jest.setup.ts"
]
},
"bugs": {
Expand Down
9 changes: 7 additions & 2 deletions packages/react-paypal-js/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import pkg from "./package.json";
const pkgName = pkg.name.split("@paypal/")[1];
const banner = getBannerText();
const tsconfigOverride = {
exclude: ["node_modules", "**/*.test.ts"],
exclude: ["node_modules", "**/*.test.ts*"],
outDir: "./dist",
target: "es5",
};

Expand All @@ -19,6 +20,7 @@ export default [
input: "src/index.ts",
plugins: [
typescript({
tsconfig: "./tsconfig.lib.json",
...tsconfigOverride,
}),
nodeResolve(),
Expand Down Expand Up @@ -56,7 +58,10 @@ export default [
{
input: "src/index.ts",
plugins: [
typescript({ ...tsconfigOverride }),
typescript({
tsconfig: "./tsconfig.lib.json",
...tsconfigOverride,
}),
nodeResolve(),
cleanup({
comments: "none",
Expand Down
65 changes: 60 additions & 5 deletions packages/react-paypal-js/src/components/PayPalButtons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fireEvent,
act,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ErrorBoundary } from "react-error-boundary";
import { mock } from "jest-mock-extended";
import { loadScript } from "@paypal/paypal-js";
Expand Down Expand Up @@ -450,17 +451,71 @@ describe("<PayPalButtons />", () => {
test("should accept button message amount as a string", async () => {
render(
<PayPalScriptProvider options={{ clientId: "test" }}>
<PayPalButtons
message={{ amount: "100" }}
/>
</PayPalScriptProvider>
<PayPalButtons message={{ amount: "100" }} />
</PayPalScriptProvider>,
);

await waitFor(() =>
expect(window.paypal?.Buttons).toHaveBeenCalledWith({
message: { amount: "100" },
onInit: expect.any(Function),
})
}),
);
});

test("should not create a stale closure when passing callbacks", async () => {
userEvent.setup();

// @ts-expect-error mocking partial ButtonComponent
window.paypal!.Buttons = ({ onClick }: { onClick: () => void }) => ({
isEligible: () => true,
close: async () => undefined,
render: async (ref: HTMLDivElement) => {
const el = document.createElement("button");
el.id = "mock-button";
el.textContent = "Pay";
el.addEventListener("click", onClick);
ref.appendChild(el);
},
});
const onClickFn = jest.fn();

const Wrapper = () => {
const [count, setCount] = useState(0);

function onClick() {
onClickFn(count);
}

return (
<div>
<button
data-testid="count-button"
onClick={() => setCount(count + 1)}
>
Count: {count}
</button>
<PayPalScriptProvider options={{ clientId: "test" }}>
<PayPalButtons onClick={onClick} />
</PayPalScriptProvider>
</div>
);
};

render(<Wrapper />);

const countButton = screen.getByTestId("count-button");
const payButton = await screen.findByText("Pay");
expect(screen.getByText("Count: 0")).toBeInTheDocument();

await userEvent.click(countButton);
expect(await screen.findByText("Count: 1")).toBeInTheDocument();
await userEvent.click(payButton);
expect(onClickFn).toHaveBeenCalledWith(1);

await userEvent.click(countButton);
expect(await screen.findByText("Count: 2")).toBeInTheDocument();
await userEvent.click(payButton);
expect(onClickFn).toHaveBeenCalledWith(2);
});
});
4 changes: 3 additions & 1 deletion packages/react-paypal-js/src/components/PayPalButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SDK_SETTINGS } from "../constants";
import type { FunctionComponent } from "react";
import type { PayPalButtonsComponent, OnInitActions } from "@paypal/paypal-js";
import type { PayPalButtonsComponentProps } from "../types";
import { useProxyProps } from "../hooks/useProxyProps";

/**
This `<PayPalButtons />` component supports rendering [buttons](https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-reference/#buttons) for PayPal, Venmo, and alternative payment methods.
Expand All @@ -25,6 +26,7 @@ export const PayPalButtons: FunctionComponent<PayPalButtonsComponentProps> = ({
}`.trim();
const buttonsContainerRef = useRef<HTMLDivElement>(null);
const buttons = useRef<PayPalButtonsComponent | null>(null);
const proxyProps = useProxyProps(buttonProps);

const [{ isResolved, options }] = usePayPalScriptReducer();
const [initActions, setInitActions] = useState<OnInitActions | null>(null);
Expand Down Expand Up @@ -86,7 +88,7 @@ export const PayPalButtons: FunctionComponent<PayPalButtonsComponentProps> = ({

try {
buttons.current = paypalWindowNamespace.Buttons({
...buttonProps,
...proxyProps,
onInit: decoratedOnInit,
});
} catch (err) {
Expand Down
57 changes: 57 additions & 0 deletions packages/react-paypal-js/src/hooks/useProxyProps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
CreateOrderActions,
CreateOrderData,
OnClickActions,
} from "@paypal/paypal-js";
import { renderHook } from "@testing-library/react-hooks";
import { useProxyProps } from "./useProxyProps";

describe("useProxyProps", () => {
test("should return an object of wrapped callbacks", () => {
const createOrder = jest.fn().mockReturnValue("createOrder");
const onClick = jest.fn().mockReturnValue("onClick");

const props = {
createOrder,
onClick,
};

const {
result: { current },
} = renderHook(() => useProxyProps(props));

expect(current).toHaveProperty("createOrder");
expect(current).toHaveProperty("onClick");
expect(current.createOrder).not.toBe(props.createOrder);
expect(current.onClick).not.toBe(props.onClick);

expect(
current.createOrder!(
{} as CreateOrderData,
{} as CreateOrderActions,
),
).toBe("createOrder");
expect(current.onClick!({}, {} as OnClickActions)).toBe("onClick");

expect(props.createOrder).toHaveBeenCalled();
expect(props.onClick).toHaveBeenCalled();

// ensure no props mutation
expect(props.createOrder).toBe(createOrder);
expect(props.onClick).toBe(onClick);
});

test("should not wrap or mutate non-function props", () => {
const fundingSource = ["paypal"];
const props = {
fundingSource,
};

const {
result: { current },
} = renderHook(() => useProxyProps(props));

expect(current.fundingSource).toBe(props.fundingSource);
expect(props.fundingSource).toBe(fundingSource);
});
});
31 changes: 31 additions & 0 deletions packages/react-paypal-js/src/hooks/useProxyProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useRef } from "react";

export function useProxyProps<T extends Record<PropertyKey, unknown>>(
props: T,
): T {
const proxyRef = useRef(
new Proxy<T>({} as T, {
get(target: T, prop: PropertyKey, 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: unknown[]) =>
// eslint-disable-next-line @typescript-eslint/ban-types
(target[prop] as Function)(...args);
}

return Reflect.get(target, prop, receiver);
},
}),
);

proxyRef.current = Object.assign(proxyRef.current, props);

return proxyRef.current;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { action } from "@storybook/addon-actions";

import { usePayPalScriptReducer, DISPATCH_ACTION } from "../../index";
Expand Down Expand Up @@ -256,3 +256,58 @@ export const Donate: FC<Omit<StoryProps, "showSpinner" | "fundingSource">> = ({
fundingSource: { control: false },
showSpinner: { table: { disable: true } },
};

export const WithDynamicOrderState: FC<StoryProps> = ({
style,
message,
fundingSource,
disabled,
showSpinner,
}) => {
const [count, setCount] = useState(0);
const [{ options }, dispatch] = usePayPalScriptReducer();
useEffect(() => {
dispatch({
type: DISPATCH_ACTION.RESET_OPTIONS,
value: {
...options,
"data-order-id": Date.now().toString(),
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showSpinner]);

return (
<>
{showSpinner && <LoadingSpinner />}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<PayPalButtons
style={style}
message={message}
disabled={disabled}
fundingSource={fundingSource}
forceReRender={[style]}
createOrder={() =>
createOrder([{ sku: "1blwyeo8", quantity: count }]).then(
(orderData) => {
if (orderData.id) {
action(ORDER_ID)(orderData.id);
return orderData.id;
} else {
throw new Error("failed to create Order Id");
}
},
)
}
onApprove={(data) =>
onApprove(data).then((orderData) =>
action(APPROVE)(orderData),
)
}
{...defaultProps}
>
<InEligibleError />
</PayPalButtons>
</>
);
};
2 changes: 1 addition & 1 deletion packages/react-paypal-js/tsconfig.declarations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["src/stories"]
"exclude": ["src/stories", "src/**/*.test.ts", "src/**/*.test.tsx"]
}
Loading

0 comments on commit 37ef35d

Please sign in to comment.