Skip to content

Commit

Permalink
feat: add deferLoading prop to control sdk script loading
Browse files Browse the repository at this point in the history
  • Loading branch information
gregjopa committed Apr 5, 2021
1 parent 06ddd6f commit 2cf3904
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 59 deletions.
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ React developers think in terms of components and not about loading external scr
**Features**

- Enforce async loading the JS SDK up front so when it's time to render the buttons to your buyer, they render immediately.
- Abstract away the complexity around loading the JS SDK with the global `<PayPalScriptProvider>` component.
- Abstract away the complexity around loading the JS SDK with the global [PayPalScriptProvider](https://paypal.github.io/react-paypal-js/?path=/docs/example-paypalscriptprovider--default) component.
- Support dispatching actions to reload the JS SDK and re-render components when global parameters like `currency` change.
- Easy to use components for all the different PayPal product offerings:
- [PayPalButtons](https://paypal.github.io/react-paypal-js/?path=/docs/example-paypalbuttons--default)
Expand Down Expand Up @@ -61,12 +61,10 @@ export default function App() {
}
```

### PayPalButtons

The `<PayPalButtons />` component is fully documented in Storybook. Checkout the [docs page for the PayPalButtons](https://paypal.github.io/react-paypal-js/?path=/docs/example-paypalbuttons--default) to learn more about the available props.

### PayPalScriptProvider

#### Options

Use the PayPalScriptProvider `options` prop to configure the JS SDK. It accepts an object for passing query parameters and data attributes to the JS SDK script.

```jsx
Expand All @@ -76,20 +74,41 @@ const initialOptions = {
intent: "capture",
"data-client-token": "abc123xyz==",
};
<PayPalScriptProvider options={initialOptions}>
<PayPalButtons />
</PayPalScriptProvider>;

export default function App() {
return (
<PayPalScriptProvider options={initialOptions}>
<PayPalButtons />
</PayPalScriptProvider>
);
}
```

The [JS SDK Configuration guide](https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-configuration/) contains the full list of query parameters and data attributes that can be used with the JS SDK.

The `<PayPalScriptProvider />` component is designed to be used with the `usePayPalScriptReducer` hook for managing global state. This `usePayPalScriptReducer` hook has the same API as [React's useReducer hook](https://reactjs.org/docs/hooks-reference.html#usereducer).
#### deferLoading

Use the optional PayPalScriptProvider `deferLoading` prop to control when the JS SDK script loads.

- This prop is set to false by default since we usually know all the sdk script params up front and want to load the script right way so components like `<PayPalButtons />` render immediately.
- This prop can be set to true to prevent loading the JS SDK script when the PayPalScriptProvider renders. Use `deferLoading={true}` initially and then dispatch an action later on in the app's life cycle to load the sdk script.

```jsx
<PayPalScriptProvider deferLoading={true} options={initialOptions}>
<PayPalButtons />
</PayPalScriptProvider>
```

To learn more, check out the [defer loading example in storybook](https://paypal.github.io/react-paypal-js/?path=/story/example-paypalscriptprovider--defer-loading).

#### Tracking loading state

The `<PayPalScriptProvider />` component is designed to be used with the `usePayPalScriptReducer` hook for managing global state. This `usePayPalScriptReducer` hook has the same API as [React's useReducer hook](https://reactjs.org/docs/hooks-reference.html#usereducer).

The `usePayPalScriptReducer` hook provides an easy way to tap into the loading state of the JS SDK script. This state can be used to show a loading spinner while the script loads or an error message if it fails to load. The following derived attributes are provided for tracking this loading state:

- isPending - not finished loading (default state)
- isInitial - not started (only used when passing `deferLoading={true}`)
- isPending - loading (default)
- isResolved - successfully loaded
- isRejected - failed to load

Expand Down Expand Up @@ -141,6 +160,10 @@ return (

To learn more, check out the [dynamic currency example in storybook](https://paypal.github.io/react-paypal-js/?path=/story/example-usepaypalscriptreducer--currency).

### PayPalButtons

The `<PayPalButtons />` component is fully documented in Storybook. Checkout the [docs page for the PayPalButtons](https://paypal.github.io/react-paypal-js/?path=/docs/example-paypalbuttons--default) to learn more about the available props.

### Browser Support

This library supports all popular browsers, including IE 11. It provides the same browser support as the JS SDK. Here's the [full list of supported browsers](https://developer.paypal.com/docs/business/checkout/reference/browser-support/#supported-browsers-by-platform).
37 changes: 37 additions & 0 deletions src/ScriptContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,43 @@ describe("<PayPalScriptProvider />", () => {
expect(state.isPending).toBeFalsy();
expect(state.isResolved).toBeFalsy();
});

test("should control script loading with the deferLoading prop", async () => {
const { state, TestComponent } = setupTestComponent();

const { rerender } = render(
<PayPalScriptProvider
deferLoading={true}
options={{ "client-id": "test" }}
>
<TestComponent />
</PayPalScriptProvider>
);

// verify initial state
expect(state.isInitial).toBe(true);
expect(loadScript).not.toHaveBeenCalled();

// re-render the same PayPalScriptProvider component with different props
rerender(
<PayPalScriptProvider
deferLoading={false}
options={{ "client-id": "test" }}
>
<TestComponent />
</PayPalScriptProvider>
);

expect(loadScript).toHaveBeenCalledWith({
"client-id": "test",
"data-react-paypal-script-id": expect.stringContaining(
"react-paypal-js"
),
});

expect(state.isPending).toBe(true);
await waitFor(() => expect(state.isResolved).toBe(true));
});
});

describe("usePayPalScriptReducer", () => {
Expand Down
21 changes: 19 additions & 2 deletions src/ScriptContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ReactPayPalScriptOptions extends PayPalScriptOptions {
}

enum SCRIPT_LOADING_STATE {
INITIAL = "initial",
PENDING = "pending",
REJECTED = "rejected",
RESOLVED = "resolved",
Expand All @@ -25,6 +26,7 @@ interface ScriptContextState {

interface ScriptContextDerivedState {
options: ReactPayPalScriptOptions;
isInitial: boolean;
isPending: boolean;
isRejected: boolean;
isResolved: boolean;
Expand Down Expand Up @@ -97,6 +99,7 @@ function usePayPalScriptReducer(): [

const derivedStatusContext = {
...restScriptContext,
isInitial: loadingStatus === SCRIPT_LOADING_STATE.INITIAL,
isPending: loadingStatus === SCRIPT_LOADING_STATE.PENDING,
isResolved: loadingStatus === SCRIPT_LOADING_STATE.RESOLVED,
isRejected: loadingStatus === SCRIPT_LOADING_STATE.REJECTED,
Expand All @@ -108,23 +111,37 @@ function usePayPalScriptReducer(): [
interface ScriptProviderProps {
options: PayPalScriptOptions;
children?: React.ReactNode;
deferLoading?: boolean;
}

const PayPalScriptProvider: FunctionComponent<ScriptProviderProps> = ({
options,
children,
deferLoading = false,
}: ScriptProviderProps) => {
const initialState = {
options: {
...options,
"data-react-paypal-script-id": `${getNewScriptID()}`,
},
loadingStatus: SCRIPT_LOADING_STATE.PENDING,
loadingStatus: deferLoading
? SCRIPT_LOADING_STATE.INITIAL
: SCRIPT_LOADING_STATE.PENDING,
};

const [state, dispatch] = useReducer(scriptReducer, initialState);

useEffect(() => {
if (
deferLoading === false &&
state.loadingStatus === SCRIPT_LOADING_STATE.INITIAL
) {
return dispatch({
type: "setLoadingStatus",
value: SCRIPT_LOADING_STATE.PENDING,
});
}

if (state.loadingStatus !== SCRIPT_LOADING_STATE.PENDING) return;

let isSubscribed = true;
Expand All @@ -148,7 +165,7 @@ const PayPalScriptProvider: FunctionComponent<ScriptProviderProps> = ({
return () => {
isSubscribed = false;
};
});
}, [options, deferLoading, state.loadingStatus]);

return (
<ScriptContext.Provider value={state}>
Expand Down
87 changes: 87 additions & 0 deletions src/stories/PayPalScriptProvider.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";
import { PayPalScriptProvider, usePayPalScriptReducer } from "../index";

export default {
title: "Example/PayPalScriptProvider",
component: PayPalScriptProvider,
};

export const Default = () => {
return (
<PayPalScriptProvider options={{ "client-id": "test" }}>
<PrintLoadingState />
{/* add your paypal components here (ex: <PayPalButtons />) */}
</PayPalScriptProvider>
);
};

export const DeferLoading = () => {
function LoadScriptButton() {
const [{ isResolved }, dispatch] = usePayPalScriptReducer();

return (
<button
type="button"
style={{ display: "block", marginBottom: "20px" }}
disabled={isResolved}
onClick={() => {
dispatch({
type: "setLoadingStatus",
value: "pending",
});
}}
>
LoadScript
</button>
);
}

return (
<PayPalScriptProvider
deferLoading={true}
options={{ "client-id": "test" }}
>
<PrintLoadingState />
<LoadScriptButton />
</PayPalScriptProvider>
);
};

function PrintLoadingState() {
const [
{ isInitial, isPending, isResolved, isRejected },
] = usePayPalScriptReducer();

console.log(isPending);

if (isInitial) {
return (
<p>
<strong>isInitial</strong> - the sdk script has not been loaded
yet. It has been deferred.{" "}
</p>
);
} else if (isPending) {
return (
<p>
<strong>isPending</strong> - the sdk script is loading.
</p>
);
} else if (isResolved) {
return (
<p>
<strong>isResolved</strong> - the sdk script has successfully
loaded.
</p>
);
} else if (isRejected) {
return (
<p>
<strong>isRejected</strong> - something went wrong. The sdk
script failed to load.
</p>
);
}

return null;
}

0 comments on commit 2cf3904

Please sign in to comment.