Skip to content

Commit 14441e5

Browse files
authored
Merge pull request #754 from paypal/feature/DTPPCPSDK-4084-cardfields-provider
CardFieldsProvider and context created
2 parents d9fb4a2 + bdc987d commit 14441e5

File tree

8 files changed

+903
-0
lines changed

8 files changed

+903
-0
lines changed

.changeset/shiny-paws-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@paypal/react-paypal-js": patch
3+
---
4+
5+
Created CardFieldsProvider and context for creating and providing Card Fields sessions
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import React from "react";
6+
import { renderToString } from "react-dom/server";
7+
8+
import { usePayPal } from "../hooks/usePayPal";
9+
import { useCardFields, useCardFieldsSession } from "../hooks/useCardFields";
10+
import { INSTANCE_LOADING_STATE } from "../types";
11+
import { isServer } from "../utils";
12+
import { CardFieldsProvider } from "./CardFieldsProvider";
13+
14+
import type {
15+
CardFieldsSessionState,
16+
CardFieldsStatusState,
17+
} from "../context/CardFieldsProviderContext";
18+
import type { CardFieldsSessionType } from "./CardFieldsProvider";
19+
20+
jest.mock("../hooks/usePayPal");
21+
22+
jest.mock("../utils", () => ({
23+
...jest.requireActual("../utils"),
24+
isServer: () => true,
25+
}));
26+
27+
const mockUsePayPal = usePayPal as jest.MockedFunction<typeof usePayPal>;
28+
29+
const oneTimePaymentSessionType: CardFieldsSessionType = "one-time-payment";
30+
const savePaymentSessionType: CardFieldsSessionType = "save-payment";
31+
32+
// Test utilites
33+
function expectInitialStatusState(state: CardFieldsStatusState): void {
34+
expect(state.cardFieldsError).toBe(null);
35+
}
36+
37+
function expectInitialSessionState(state: CardFieldsSessionState): void {
38+
expect(state.cardFieldsSession).toBe(null);
39+
}
40+
41+
// Render helpers
42+
function renderSSRCardFieldsProvider({
43+
sessionType,
44+
children,
45+
}: {
46+
sessionType: CardFieldsSessionType;
47+
children?: React.ReactNode;
48+
}) {
49+
const { cardFieldsSessionState, cardFieldsStatusState, TestComponent } =
50+
setupSSRTestComponent();
51+
52+
const html = renderToString(
53+
<CardFieldsProvider sessionType={sessionType}>
54+
<TestComponent>{children}</TestComponent>
55+
</CardFieldsProvider>,
56+
);
57+
58+
return { html, cardFieldsSessionState, cardFieldsStatusState };
59+
}
60+
61+
function setupSSRTestComponent() {
62+
const cardFieldsStatusState: CardFieldsStatusState = {
63+
cardFieldsError: null,
64+
};
65+
66+
const cardFieldsSessionState: CardFieldsSessionState = {
67+
cardFieldsSession: null,
68+
};
69+
70+
function TestComponent({
71+
children = null,
72+
}: {
73+
children?: React.ReactNode;
74+
}) {
75+
const newCardFieldsStatusState = useCardFields();
76+
const newCardFieldsSessionState = useCardFieldsSession();
77+
78+
Object.assign(cardFieldsStatusState, newCardFieldsStatusState);
79+
Object.assign(cardFieldsSessionState, newCardFieldsSessionState);
80+
81+
return <>{children}</>;
82+
}
83+
84+
return { cardFieldsStatusState, cardFieldsSessionState, TestComponent };
85+
}
86+
87+
describe("CardFieldsProvider SSR", () => {
88+
beforeEach(() => {
89+
mockUsePayPal.mockReturnValue({
90+
sdkInstance: null,
91+
loadingStatus: INSTANCE_LOADING_STATE.PENDING,
92+
eligiblePaymentMethods: null,
93+
error: null,
94+
});
95+
});
96+
97+
afterEach(() => {
98+
jest.clearAllMocks();
99+
});
100+
101+
describe("Server-Side Rendering", () => {
102+
test("should verify isServer mock is working", () => {
103+
expect(isServer()).toBe(true);
104+
});
105+
106+
test("should not attempt DOM access during server rendering", () => {
107+
// In Node environment, document should be undefined
108+
expect(typeof document).toBe("undefined");
109+
110+
// Will throw error if any DOM access is attempted during render
111+
expect(() =>
112+
renderSSRCardFieldsProvider({
113+
sessionType: oneTimePaymentSessionType,
114+
}),
115+
).not.toThrow();
116+
});
117+
118+
test("should work correctly in SSR context", () => {
119+
const { cardFieldsSessionState, cardFieldsStatusState } =
120+
renderSSRCardFieldsProvider({
121+
sessionType: oneTimePaymentSessionType,
122+
});
123+
124+
expectInitialStatusState(cardFieldsStatusState);
125+
expectInitialSessionState(cardFieldsSessionState);
126+
});
127+
128+
test("should render without warnings or errors", () => {
129+
// This sdkInstance state would fail client side
130+
mockUsePayPal.mockReturnValue({
131+
sdkInstance: null,
132+
loadingStatus: INSTANCE_LOADING_STATE.REJECTED,
133+
eligiblePaymentMethods: null,
134+
error: null,
135+
});
136+
137+
const consoleSpy = jest
138+
.spyOn(console, "error")
139+
.mockImplementation();
140+
141+
const { html } = renderSSRCardFieldsProvider({
142+
sessionType: oneTimePaymentSessionType,
143+
children: <div>SSR Content</div>,
144+
});
145+
146+
expect(consoleSpy).not.toHaveBeenCalled();
147+
expect(html).toBeTruthy();
148+
expect(typeof html).toBe("string");
149+
expect(html).toContain("SSR Content");
150+
151+
consoleSpy.mockRestore();
152+
});
153+
});
154+
155+
describe("Hydration Preparation", () => {
156+
test("should prepare serializable state for client hydration", () => {
157+
const { cardFieldsSessionState, cardFieldsStatusState } =
158+
renderSSRCardFieldsProvider({
159+
sessionType: oneTimePaymentSessionType,
160+
});
161+
162+
// Server state should be safe for serialization
163+
const serializedSessionState = JSON.stringify(
164+
cardFieldsSessionState,
165+
);
166+
const serializedStatusState = JSON.stringify(cardFieldsStatusState);
167+
168+
expect(() => JSON.parse(serializedSessionState)).not.toThrow();
169+
expect(() => JSON.parse(serializedStatusState)).not.toThrow();
170+
171+
const parsedSessionState = JSON.parse(serializedSessionState);
172+
const parsedStatusState = JSON.parse(serializedStatusState);
173+
174+
expectInitialSessionState(parsedSessionState);
175+
expectInitialStatusState(parsedStatusState);
176+
});
177+
178+
test("should maintain consistent state with different options", () => {
179+
const {
180+
cardFieldsStatusState: cardFieldsStatusState1,
181+
cardFieldsSessionState: cardFieldsSessionState1,
182+
} = renderSSRCardFieldsProvider({
183+
sessionType: oneTimePaymentSessionType,
184+
});
185+
const {
186+
cardFieldsStatusState: cardFieldsStatusState2,
187+
cardFieldsSessionState: cardFieldsSessionState2,
188+
} = renderSSRCardFieldsProvider({
189+
sessionType: savePaymentSessionType,
190+
});
191+
192+
// Both should have consistent initial state regardless of options
193+
expectInitialStatusState(cardFieldsStatusState1);
194+
expectInitialSessionState(cardFieldsSessionState1);
195+
196+
expectInitialStatusState(cardFieldsStatusState2);
197+
expectInitialSessionState(cardFieldsSessionState2);
198+
});
199+
});
200+
201+
describe("Multiple renders", () => {
202+
test("should maintain state consistency across multiple server renders", () => {
203+
const {
204+
html: html1,
205+
cardFieldsStatusState: cardFieldsStatusState1,
206+
cardFieldsSessionState: cardFieldsSessionState1,
207+
} = renderSSRCardFieldsProvider({
208+
sessionType: oneTimePaymentSessionType,
209+
});
210+
const {
211+
html: html2,
212+
cardFieldsStatusState: cardFieldsStatusState2,
213+
cardFieldsSessionState: cardFieldsSessionState2,
214+
} = renderSSRCardFieldsProvider({
215+
sessionType: oneTimePaymentSessionType,
216+
});
217+
218+
expectInitialStatusState(cardFieldsStatusState1);
219+
expectInitialSessionState(cardFieldsSessionState1);
220+
expectInitialStatusState(cardFieldsStatusState2);
221+
expectInitialSessionState(cardFieldsSessionState2);
222+
expect(html1).toBe(html2);
223+
});
224+
225+
test("should generate consistent HTML across renders", () => {
226+
const { html: html1 } = renderSSRCardFieldsProvider({
227+
sessionType: oneTimePaymentSessionType,
228+
children: <div data-testid="ssr-content">Test Content</div>,
229+
});
230+
const { html: html2 } = renderSSRCardFieldsProvider({
231+
sessionType: oneTimePaymentSessionType,
232+
children: <div data-testid="ssr-content">Test Content</div>,
233+
});
234+
235+
expect(html1).toBe(html2);
236+
expect(html1).toContain('data-testid="ssr-content"');
237+
});
238+
});
239+
240+
describe("Context isolation", () => {
241+
const expectedSessionContextKeys = ["cardFieldsSession"] as const;
242+
const expectedStatusContextKeys = ["cardFieldsError"] as const;
243+
244+
describe("useCardFields", () => {
245+
test("should only return status context values", () => {
246+
const { cardFieldsStatusState } = renderSSRCardFieldsProvider({
247+
sessionType: oneTimePaymentSessionType,
248+
});
249+
250+
const receivedStatusKeys = Object.keys(cardFieldsStatusState);
251+
expect(receivedStatusKeys.sort()).toEqual(
252+
[...expectedStatusContextKeys].sort(),
253+
);
254+
});
255+
256+
test("should not return session context values", () => {
257+
const { cardFieldsStatusState } = renderSSRCardFieldsProvider({
258+
sessionType: oneTimePaymentSessionType,
259+
});
260+
261+
const receivedStatusKeys = Object.keys(cardFieldsStatusState);
262+
expectedSessionContextKeys.forEach((key) => {
263+
expect(receivedStatusKeys).not.toContain(key);
264+
});
265+
});
266+
});
267+
268+
describe("useCardFieldsSession", () => {
269+
test("should only return session context values", () => {
270+
const { cardFieldsSessionState } = renderSSRCardFieldsProvider({
271+
sessionType: oneTimePaymentSessionType,
272+
});
273+
274+
const receivedSessionKeys = Object.keys(cardFieldsSessionState);
275+
expect(receivedSessionKeys.sort()).toEqual(
276+
[...expectedSessionContextKeys].sort(),
277+
);
278+
});
279+
280+
test("should not return status context values", () => {
281+
const { cardFieldsSessionState } = renderSSRCardFieldsProvider({
282+
sessionType: oneTimePaymentSessionType,
283+
});
284+
285+
const receivedSessionKeys = Object.keys(cardFieldsSessionState);
286+
expectedStatusContextKeys.forEach((key) => {
287+
expect(receivedSessionKeys).not.toContain(key);
288+
});
289+
});
290+
});
291+
});
292+
});

0 commit comments

Comments
 (0)