-
Notifications
You must be signed in to change notification settings - Fork 121
Feat: V6 Context Provider #688
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
Changes from all commits
07b5887
fe5feee
8f8c959
5110af3
ba846db
ede8236
414227f
eba55a7
3fa346e
599f85f
f5333b2
853694b
c59a0bb
cfbfb0e
f27e7f8
b532494
934c25a
0005534
bea46b3
3f7ea83
fe1e293
4755ad3
ace1a38
e5dc00e
8397495
b5145df
07dd9d1
7ffdce2
2093f29
c96a7e2
604c6dc
12b2431
f987569
e19df62
3563130
c96c47d
7449a90
77c56b9
8e624e3
ed2b6ac
1f7404c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@paypal/react-paypal-js": minor | ||
"@paypal/paypal-js": minor | ||
--- | ||
|
||
Add V6 instance provider and context hook |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { insertScriptElement } from "../utils"; | ||
import { insertScriptElement, isServer } from "../utils"; | ||
import type { | ||
PayPalV6Namespace, | ||
LoadCoreSdkScriptOptions, | ||
|
@@ -8,6 +8,16 @@ const version = "__VERSION__"; | |
|
||
function loadCoreSdkScript(options: LoadCoreSdkScriptOptions = {}) { | ||
validateArguments(options); | ||
const isServerEnv = isServer(); | ||
|
||
// SSR safeguard - warn about incorrect usage | ||
if (isServerEnv) { | ||
console.warn( | ||
"PayPal JS: loadCoreSdkScript() was called on the server. This function should only be called on the client side. Please ensure you're not calling this during server-side rendering.", | ||
); | ||
return Promise.resolve(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great point on adding a warning! |
||
} | ||
|
||
const { environment, debug } = options; | ||
|
||
const baseURL = | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
/** | ||
* @jest-environment node | ||
*/ | ||
|
||
import React from "react"; | ||
import { renderToString } from "react-dom/server"; | ||
import { loadCoreSdkScript } from "@paypal/paypal-js/sdk-v6"; | ||
|
||
import { PayPalProvider } from "./PayPalProvider"; | ||
import { usePayPal } from "../hooks/usePayPal"; | ||
import { INSTANCE_LOADING_STATE } from "../types/PayPalProviderTypes"; | ||
import { isServer } from "../utils"; | ||
import { TEST_CLIENT_TOKEN, expectInitialState } from "./providerTestUtils"; | ||
|
||
import type { | ||
CreateInstanceOptions, | ||
PayPalContextState, | ||
LoadCoreSdkScriptOptions, | ||
} from "../types"; | ||
|
||
jest.mock("@paypal/paypal-js/sdk-v6", () => ({ | ||
loadCoreSdkScript: jest.fn(), | ||
})); | ||
|
||
jest.mock("../utils", () => ({ | ||
...jest.requireActual("../utils"), | ||
isServer: () => true, | ||
})); | ||
|
||
const createInstanceOptions: CreateInstanceOptions<["paypal-payments"]> = { | ||
components: ["paypal-payments"], | ||
clientToken: TEST_CLIENT_TOKEN, | ||
}; | ||
|
||
const scriptOptions: LoadCoreSdkScriptOptions = { | ||
environment: "sandbox", | ||
}; | ||
|
||
// Test utilities | ||
function renderSSRProvider( | ||
instanceOptions = createInstanceOptions, | ||
scriptOpts = scriptOptions, | ||
children?: React.ReactNode, | ||
) { | ||
const { state, TestComponent } = setupSSRTestComponent(); | ||
const { components, clientToken } = instanceOptions; | ||
|
||
const html = renderToString( | ||
<PayPalProvider | ||
components={components} | ||
clientToken={clientToken} | ||
scriptOptions={scriptOpts} | ||
> | ||
<TestComponent>{children}</TestComponent> | ||
</PayPalProvider>, | ||
); | ||
|
||
return { html, state }; | ||
} | ||
|
||
describe("PayPalProvider SSR", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test("should verify isServer mock is working", () => { | ||
expect(isServer()).toBe(true); | ||
}); | ||
|
||
test("should initialize correctly and never load scripts during SSR", () => { | ||
const { state } = renderSSRProvider(); | ||
|
||
expectInitialState(state); | ||
expect(loadCoreSdkScript).not.toHaveBeenCalled(); | ||
}); | ||
|
||
describe("Server-Side Rendering", () => { | ||
test("should not attempt DOM access during server rendering", () => { | ||
// In Node environment, document should be undefined | ||
expect(typeof document).toBe("undefined"); | ||
|
||
expect(() => renderSSRProvider()).not.toThrow(); | ||
}); | ||
|
||
test("should maintain state consistency across multiple server renders", () => { | ||
const { html: html1, state: state1 } = renderSSRProvider(); | ||
const { html: html2, state: state2 } = renderSSRProvider(); | ||
|
||
// Both renders should produce identical state and HTML | ||
expectInitialState(state1); | ||
expectInitialState(state2); | ||
expect(html1).toBe(html2); | ||
}); | ||
}); | ||
|
||
describe("Hydration Preparation", () => { | ||
test("should prepare serializable state for client hydration", () => { | ||
const { state } = renderSSRProvider(); | ||
|
||
// Server state should be safe for serialization | ||
const serializedState = JSON.stringify({ | ||
loadingStatus: state.loadingStatus, | ||
sdkInstance: state.sdkInstance, | ||
eligiblePaymentMethods: state.eligiblePaymentMethods, | ||
error: state.error, | ||
}); | ||
|
||
expect(() => JSON.parse(serializedState)).not.toThrow(); | ||
|
||
const parsedState = JSON.parse(serializedState); | ||
expectInitialState(parsedState); | ||
}); | ||
|
||
test("should handle different options consistently", () => { | ||
const updatedOptions = { | ||
...createInstanceOptions, | ||
clientToken: "updated-token", | ||
}; | ||
|
||
const { state: state1 } = renderSSRProvider(); | ||
const { state: state2 } = renderSSRProvider(updatedOptions); | ||
|
||
// Both should have consistent initial state regardless of options | ||
expectInitialState(state1); | ||
expectInitialState(state2); | ||
expect(loadCoreSdkScript).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe("React SSR Integration", () => { | ||
test("should render without warnings or errors", () => { | ||
const consoleSpy = jest | ||
.spyOn(console, "error") | ||
.mockImplementation(); | ||
|
||
const { html } = renderSSRProvider( | ||
createInstanceOptions, | ||
scriptOptions, | ||
<div>SSR Content</div>, | ||
); | ||
|
||
expect(consoleSpy).not.toHaveBeenCalled(); | ||
expect(html).toBeTruthy(); | ||
expect(typeof html).toBe("string"); | ||
expect(html).toContain("SSR Content"); | ||
|
||
consoleSpy.mockRestore(); | ||
}); | ||
|
||
test("should handle complex options without issues", () => { | ||
const complexOptions: CreateInstanceOptions< | ||
["paypal-payments", "venmo-payments"] | ||
> = { | ||
components: ["paypal-payments", "venmo-payments"], | ||
clientToken: "complex-token-123", | ||
locale: "en_US", | ||
pageType: "checkout", | ||
partnerAttributionId: "test-partner", | ||
}; | ||
|
||
const { html } = renderSSRProvider( | ||
// @ts-expect-error renderSSRProvider is typed for single component | ||
complexOptions, | ||
{ environment: "production", debug: true }, | ||
<div>Complex Options Test</div>, | ||
); | ||
|
||
expect(html).toBeTruthy(); | ||
expect(html).toContain("Complex Options Test"); | ||
}); | ||
|
||
test("should generate consistent HTML across renders", () => { | ||
const { html: html1 } = renderSSRProvider( | ||
createInstanceOptions, | ||
scriptOptions, | ||
<div data-testid="ssr-content">Test Content</div>, | ||
); | ||
const { html: html2 } = renderSSRProvider( | ||
createInstanceOptions, | ||
scriptOptions, | ||
<div data-testid="ssr-content">Test Content</div>, | ||
); | ||
|
||
expect(html1).toBe(html2); | ||
expect(html1).toContain('data-testid="ssr-content"'); | ||
}); | ||
}); | ||
|
||
describe("Multiple Renders", () => { | ||
test("should handle multiple renders without side effects", () => { | ||
// Multiple renders should not cause issues | ||
expect(() => { | ||
for (let i = 0; i < 5; i++) { | ||
renderSSRProvider({ | ||
...createInstanceOptions, | ||
clientToken: `token-${i}`, | ||
}); | ||
} | ||
}).not.toThrow(); | ||
|
||
// Should never attempt to load scripts regardless of render count | ||
expect(loadCoreSdkScript).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("usePayPalInstance SSR", () => { | ||
test("should work correctly in SSR context", () => { | ||
const { state } = renderSSRProvider(); | ||
|
||
expectInitialState(state); | ||
}); | ||
}); | ||
|
||
function setupSSRTestComponent() { | ||
const state: PayPalContextState = { | ||
loadingStatus: INSTANCE_LOADING_STATE.INITIAL, | ||
sdkInstance: null, | ||
eligiblePaymentMethods: null, | ||
error: null, | ||
dispatch: jest.fn(), | ||
}; | ||
|
||
function TestComponent({ | ||
children = null, | ||
}: { | ||
children?: React.ReactNode; | ||
}) { | ||
try { | ||
const instanceState = usePayPal(); | ||
Object.assign(state, instanceState); | ||
} catch (error) { | ||
state.error = error as Error; | ||
} | ||
return <>{children}</>; | ||
} | ||
|
||
return { | ||
state, | ||
TestComponent, | ||
}; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.