diff --git a/.storybook/preview.js b/.storybook/preview.js
index 218efb75..3c307c1e 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -6,7 +6,9 @@ export const parameters = {
"PayPal",
[
"PayPalButtons",
+ "PayPalCardFields",
"PayPalHostedFields",
+ "PayPalHostedFieldsProvider",
"PayPalMarks",
"PayPalMessages",
"Subscriptions",
diff --git a/src/components/cardFields/PayPalCardFieldsForm.tsx b/src/components/cardFields/PayPalCardFieldsForm.tsx
index 5b24c5b3..b4a11616 100644
--- a/src/components/cardFields/PayPalCardFieldsForm.tsx
+++ b/src/components/cardFields/PayPalCardFieldsForm.tsx
@@ -5,6 +5,14 @@ import { PayPalCardField } from "./PayPalCardField";
import { FlexContainer } from "../ui/FlexContainer";
import { FullWidthContainer } from "../ui/FullWidthContainer";
+/**
+This `` component renders the 4 individual fields for [Card Fields](https://developer.paypal.com/docs/business/checkout/advanced-card-payments/integrate#3-add-javascript-sdk-and-card-form) integrations.
+This setup relies on the `` parent component, which manages the state related to loading the JS SDK script and performs certain validations before rendering the fields.
+
+
+
+Note: If you want to have more granular control over the layout of how the fields are rendered, you can alternatively use our Individual Fields.
+*/
export const PayPalCardFieldsForm: React.FC = ({
className,
...options
diff --git a/src/components/cardFields/PayPalCardFieldsProvider.tsx b/src/components/cardFields/PayPalCardFieldsProvider.tsx
index 7865e354..bf8f244c 100644
--- a/src/components/cardFields/PayPalCardFieldsProvider.tsx
+++ b/src/components/cardFields/PayPalCardFieldsProvider.tsx
@@ -17,6 +17,17 @@ type CardFieldsProviderProps = PayPalCardFieldsComponentOptions & {
children: ReactNode;
};
+/**
+The `` is a context provider that is designed to support the rendering and state management of PayPal CardFields in your application.
+
+The context provider will initialize the `CardFields` instance from the JS SDK and determine eligibility to render the CardField components. Once the `CardFields` are initialized, the context provider will manage the state of the `CardFields` instance as well as the reference to each individual card field.
+
+Passing the `inputEvents` and `style` props to the context provider will apply them to each of the individual field components.
+
+The state managed by the provider is accessible through our custom hook `usePayPalCardFields`.
+
+*/
+
export const PayPalCardFieldsProvider = ({
children,
...props
diff --git a/src/stories/payPalCardFields/code.ts b/src/stories/payPalCardFields/code.ts
new file mode 100644
index 00000000..23788d37
--- /dev/null
+++ b/src/stories/payPalCardFields/code.ts
@@ -0,0 +1,238 @@
+import { CREATE_ORDER_URL, CAPTURE_ORDER_URL } from "../utils";
+
+export const getFormCode = (): string => {
+ return `
+ import React, { useState } from "react";
+ import type { CardFieldsOnApproveData } from "@paypal/paypal-js";
+
+ import {
+ PayPalScriptProvider,
+ usePayPalCardFields,
+ PayPalCardFieldsProvider,
+ PayPalCardFieldsForm,
+ } from "@paypal/react-paypal-js";
+
+ export default function App(): JSX.Element {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ return fetch("${CREATE_ORDER_URL}", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ return order.id;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ fetch("${CAPTURE_ORDER_URL}", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+ {/* Custom client component to handle card fields submit */}
+
+
+
+ );
+ }
+
+ const SubmitPayment: React.FC<{
+ setIsPaying: React.Dispatch>;
+ isPaying: boolean;
+ }> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm, fields } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the ";
+
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+ };
+
+ `;
+};
+
+export const getIndividualFieldCode = (): string => {
+ return `
+ import React, { useState } from "react";
+ import type { CardFieldsOnApproveData } from "@paypal/paypal-js";
+
+ import {
+ PayPalScriptProvider,
+ usePayPalCardFields,
+ PayPalCardFieldsProvider,
+ PayPalCVVField,
+ PayPalExpiryField,
+ PayPalNameField,
+ PayPalNumberField,
+ } from "@paypal/react-paypal-js";
+
+ export default function App(): JSX.Element {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ return fetch("${CREATE_ORDER_URL}", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ return order.id;
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ fetch("${CAPTURE_ORDER_URL}", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+
+
+
+
+
+
+ );
+ }
+
+ const SubmitPayment: React.FC<{
+ setIsPaying: React.Dispatch>;
+ isPaying: boolean;
+ }> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the < PayPalCardFieldsProvider />";
+
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+ };
+
+ `;
+};
diff --git a/src/stories/payPalCardFields/payPalCardFieldsForm.stories.tsx b/src/stories/payPalCardFields/payPalCardFieldsForm.stories.tsx
new file mode 100644
index 00000000..530ef454
--- /dev/null
+++ b/src/stories/payPalCardFields/payPalCardFieldsForm.stories.tsx
@@ -0,0 +1,188 @@
+import React, { useState } from "react";
+import { action } from "@storybook/addon-actions";
+
+import {
+ PayPalScriptProvider,
+ usePayPalCardFields,
+ PayPalCardFieldsProvider,
+ PayPalCardFieldsForm,
+} from "../../index";
+import {
+ getOptionsFromQueryString,
+ generateRandomString,
+ CREATE_ORDER_URL,
+ CAPTURE_ORDER_URL,
+} from "../utils";
+import { ORDER_ID, ERROR } from "../constants";
+import DocPageStructure from "../components/DocPageStructure";
+import { getFormCode } from "./code";
+
+import type { DocsContextProps } from "@storybook/addon-docs";
+import type { StoryFn } from "@storybook/react";
+import type { FC } from "react";
+import type {
+ CardFieldsOnApproveData,
+ PayPalScriptOptions,
+} from "@paypal/paypal-js";
+
+const uid = generateRandomString();
+
+const scriptProviderOptions: PayPalScriptOptions = {
+ clientId:
+ "AduyjUJ0A7urUcWtGCTjanhRBSzOSn9_GKUzxWDnf51YaV1eZNA0ZAFhebIV_Eq-daemeI7dH05KjLWm",
+ components: "card-fields",
+ ...getOptionsFromQueryString(),
+};
+const CREATE_ORDER = "createOrder";
+const SUBMIT_FORM = "submitForm";
+const CAPTURE_ORDER = "captureOrder";
+
+/**
+ * Functional component to submit the card fields form
+ */
+
+const SubmitPayment: React.FC<{
+ isPaying: boolean;
+ setIsPaying: React.Dispatch>;
+}> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the ";
+
+ action(ERROR)(childErrorMessage);
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ action(SUBMIT_FORM)("Form is valid and submitted");
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ action(ERROR)(err.message);
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default {
+ title: "PayPal/PayPalCardFields/Form",
+ component: PayPalCardFieldsForm,
+ parameters: {
+ controls: { expanded: true, sort: "requiredFirst" },
+ docs: {
+ source: { language: "tsx" },
+ },
+ },
+ argTypes: {
+ className: {
+ control: false,
+ table: { category: "Props", type: { summary: "string?" } },
+ description:
+ "Classes applied to the form container, not the individual fields.",
+ defaultValue: {
+ summary: "undefined",
+ },
+ },
+ },
+};
+
+export const Default: FC = () => {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ action(CREATE_ORDER)("Start creating the order in custom endpoint");
+ return fetch(CREATE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ action(CREATE_ORDER)(order);
+ return order.id;
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ action(`Received ${ORDER_ID}`)(data.orderID);
+ action(CAPTURE_ORDER)(
+ `Sending ${ORDER_ID} to custom endpoint to capture the payment information`
+ );
+ fetch(CAPTURE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ action(CAPTURE_ORDER)(data);
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+ {/* Custom client component to handle card fields submit */}
+
+
+
+ );
+};
+
+/********************
+ * OVERRIDE STORIES *
+ *******************/
+(Default as StoryFn).parameters = {
+ docs: {
+ container: ({ context }: { context: DocsContextProps }) => (
+
+ ),
+ },
+};
diff --git a/src/stories/payPalCardFields/payPalCardFieldsIndividual.stories.tsx b/src/stories/payPalCardFields/payPalCardFieldsIndividual.stories.tsx
new file mode 100644
index 00000000..322696ed
--- /dev/null
+++ b/src/stories/payPalCardFields/payPalCardFieldsIndividual.stories.tsx
@@ -0,0 +1,315 @@
+import React, { useState } from "react";
+import { action } from "@storybook/addon-actions";
+
+import {
+ PayPalScriptProvider,
+ usePayPalCardFields,
+ PayPalCardFieldsProvider,
+ PayPalCVVField,
+ PayPalExpiryField,
+ PayPalNameField,
+ PayPalNumberField,
+} from "../../index";
+import {
+ getOptionsFromQueryString,
+ generateRandomString,
+ CREATE_ORDER_URL,
+ CAPTURE_ORDER_URL,
+} from "../utils";
+import {
+ COMPONENT_PROPS_CATEGORY,
+ COMPONENT_TYPES,
+ ORDER_ID,
+ ERROR,
+} from "../constants";
+import { getIndividualFieldCode } from "./code";
+import DocPageStructure from "../components/DocPageStructure";
+
+import type { DocsContextProps } from "@storybook/addon-docs";
+import type { FC } from "react";
+import type {
+ CardFieldsOnApproveData,
+ PayPalScriptOptions,
+} from "@paypal/paypal-js";
+import type { StoryFn } from "@storybook/react";
+
+const uid = generateRandomString();
+const scriptProviderOptions: PayPalScriptOptions = {
+ clientId:
+ "AduyjUJ0A7urUcWtGCTjanhRBSzOSn9_GKUzxWDnf51YaV1eZNA0ZAFhebIV_Eq-daemeI7dH05KjLWm",
+ components: "card-fields",
+ ...getOptionsFromQueryString(),
+};
+const CREATE_ORDER = "createOrder";
+const SUBMIT_FORM = "submitForm";
+const CAPTURE_ORDER = "captureOrder";
+
+const description = `Rendering individual fields allows for more granular control over each component as well as flexibility in the layout of all [Card Fields](https://developer.paypal.com/docs/business/checkout/advanced-card-payments/integrate#3-add-javascript-sdk-and-card-form).
+
+This setup relies on the \`\` parent component, which manages the state related to loading the JS SDK script and performs certain validations before rendering the fields.
+
+The individual fields include following components:
+
+- \`\` _optional_
+- \`\` _required_
+- \`\` _required_
+- \`\` _required_
+
+Each field accepts it's own independent props, such as \`className\`, \`placeholder\`, \`inputEvents\`, \`style\`. `;
+
+/**
+ * Functional component to submit the card fields form
+ */
+
+const SubmitPayment: React.FC<{
+ isPaying: boolean;
+ setIsPaying: React.Dispatch>;
+}> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the ";
+
+ action(ERROR)(childErrorMessage);
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ action(SUBMIT_FORM)("Form is valid and submitted");
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ action(ERROR)(err.message);
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default {
+ title: "PayPal/PayPalCardFields/Individual Fields",
+ parameters: {
+ controls: { expanded: true, sort: "requiredFirst" },
+ docs: {
+ source: { language: "tsx" },
+ description: {
+ component: description,
+ },
+ },
+ },
+ argTypes: {
+ className: {
+ control: false,
+ table: { category: "Props", type: { summary: "string?" } },
+ description: "Classes applied to individual field container.",
+ defaultValue: {
+ summary: "undefined",
+ },
+ },
+ style: {
+ description:
+ "Custom CSS properties to customize each individual card field. [Supported CSS properties](https://developer.paypal.com/docs/checkout/advanced/customize/card-field-style/#link-supportedcssproperties).",
+ control: { type: "object" },
+ ...{
+ ...COMPONENT_PROPS_CATEGORY,
+ type: { summary: "CSSProperties" },
+ },
+ },
+ inputEvents: {
+ control: false,
+ table: { category: "Props", type: { summary: "InputEvents?" } },
+ description:
+ "An object containing callbacks for when a specified input event occurs for a field.",
+ },
+ placeholder: {
+ control: false,
+ table: { category: "Props", type: { summary: "string?" } },
+ description:
+ "Each card field has a default placeholder text. Pass a placeholder object to customize this text.",
+ defaultValue: {
+ summary: "undefined",
+ },
+ },
+ InputEvents: {
+ control: false,
+ type: { required: false },
+ description: `{
+ onChange: (data: PayPalCardFieldsStateObject) => void
+ onBlur: (data: PayPalCardFieldsStateObject) => void
+ onFocus: (data: PayPalCardFieldsStateObject) => void
+ onInputSubmitRequest: (data: PayPalCardFieldsStateObject) => void
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObjectFields: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cardCvvField: PayPalCardFieldCardFieldData
+ cardNumberField: PayPalCardFieldCardFieldData
+ cardNameField: PayPalCardFieldCardFieldData
+ cardExpiryField: PayPalCardFieldCardFieldData
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cards: PayPalCardFieldsCardObject[]
+ emittedBy?: "name" | "number" | "cvv" | "expiry"
+ isFormValid: boolean
+ errors: PayPalCardFieldError[]
+ fields: PayPalCardFieldsStateObjectFields
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsCardObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: PayPalCardFieldSecurityCode
+ niceType: string
+ type: "american-express" | "diners-club" | "discover" | "jcb" | "maestro" | "mastercard" | "unionpay" | "visa" | "elo" | "hiper" | "hipercard"
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldError: {
+ control: false,
+ type: { required: false },
+ description: `"INELIGIBLE_CARD_VENDOR" | "INVALID_NAME" | "INVALID_NUMBER" | "INVALID_EXPIRY" | "INVALID_CVV"
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldSecurityCode: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: string
+ size: number
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldCardFieldData: {
+ control: false,
+ type: { required: false },
+ description: `{
+ isFocused: boolean
+ isEmpty: boolean
+ isValid: boolean
+ isPotentiallyValid: boolean
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ },
+};
+
+export const Default: FC = () => {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ action(CREATE_ORDER)("Start creating the order in custom endpoint");
+ return fetch(CREATE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ action(CREATE_ORDER)(order);
+ return order.id;
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ action(`Received ${ORDER_ID}`)(data.orderID);
+ action(CAPTURE_ORDER)(
+ `Sending ${ORDER_ID} to custom endpoint to capture the payment information`
+ );
+ fetch(CAPTURE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ action(CAPTURE_ORDER)(data);
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+
+
+
+ {/* Custom client component to handle card fields submit */}
+
+
+
+ );
+};
+
+/********************
+ * OVERRIDE STORIES *
+ *******************/
+(Default as StoryFn).parameters = {
+ docs: {
+ container: ({ context }: { context: DocsContextProps }) => (
+
+ ),
+ },
+};
diff --git a/src/stories/payPalCardFields/payPalCardFieldsProvider.stories.tsx b/src/stories/payPalCardFields/payPalCardFieldsProvider.stories.tsx
new file mode 100644
index 00000000..9b5f2e21
--- /dev/null
+++ b/src/stories/payPalCardFields/payPalCardFieldsProvider.stories.tsx
@@ -0,0 +1,333 @@
+import React, { useState } from "react";
+import { action } from "@storybook/addon-actions";
+
+import {
+ PayPalScriptProvider,
+ PayPalCardFieldsProvider,
+ PayPalCardFieldsForm,
+ usePayPalCardFields,
+} from "../../index";
+import {
+ getOptionsFromQueryString,
+ generateRandomString,
+ CREATE_ORDER_URL,
+ CAPTURE_ORDER_URL,
+} from "../utils";
+import {
+ COMPONENT_PROPS_CATEGORY,
+ COMPONENT_TYPES,
+ ORDER_ID,
+ ERROR,
+} from "../constants";
+import { getFormCode } from "./code";
+import DocPageStructure from "../components/DocPageStructure";
+
+import type { FC } from "react";
+import type {
+ CardFieldsOnApproveData,
+ PayPalScriptOptions,
+} from "@paypal/paypal-js";
+import type { StoryFn } from "@storybook/react";
+import type { DocsContextProps } from "@storybook/addon-docs";
+
+const uid = generateRandomString();
+const scriptProviderOptions: PayPalScriptOptions = {
+ clientId:
+ "AduyjUJ0A7urUcWtGCTjanhRBSzOSn9_GKUzxWDnf51YaV1eZNA0ZAFhebIV_Eq-daemeI7dH05KjLWm",
+ components: "card-fields",
+ ...getOptionsFromQueryString(),
+};
+const CREATE_ORDER = "createOrder";
+const SUBMIT_FORM = "submitForm";
+const CAPTURE_ORDER = "captureOrder";
+
+/**
+ * Functional component to submit the hosted fields form
+ */
+
+const SubmitPayment: React.FC<{
+ isPaying: boolean;
+ setIsPaying: React.Dispatch>;
+}> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the ";
+
+ action(ERROR)(childErrorMessage);
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ action(SUBMIT_FORM)("Form is valid and submitted");
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ action(ERROR)(err.message);
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default {
+ title: "PayPal/PayPalCardFields/Context Provider",
+ component: PayPalCardFieldsProvider,
+ parameters: {
+ controls: { expanded: true, sort: "requiredFirst" },
+ docs: {
+ source: { language: "tsx" },
+ disabled: true,
+ },
+ },
+ argTypes: {
+ createOrder: {
+ control: false,
+ type: {
+ required: true,
+ },
+ table: {
+ category: "Props",
+ type: { summary: "() => Promise" },
+ },
+ description:
+ "The callback to create the order on your server. [CardFields options documentation](https://developer.paypal.com/sdk/js/reference/#link-options)",
+ },
+ onApprove: {
+ control: false,
+ type: {
+ required: true,
+ },
+ table: {
+ category: "Props",
+ type: { summary: "(data: CardFieldsOnApproveData) => void" },
+ },
+ description:
+ "The callback to capture the order on your server. [CardFields options documentation](https://developer.paypal.com/sdk/js/reference/#link-options)",
+ },
+ onError: {
+ control: false,
+ type: {
+ required: true,
+ },
+ table: {
+ category: "Props",
+ type: { summary: "(err: Record) => void" },
+ },
+ description:
+ "The callback to catch errors during checkout. [CardFields options documentation](https://developer.paypal.com/sdk/js/reference/#link-options)",
+ },
+ createVaultSetupToken: {
+ control: false,
+ type: {
+ required: false,
+ },
+ table: {
+ category: "Props",
+ type: { summary: "() => Promise" },
+ },
+ description:
+ "The callback to create the `vaultSetupToken` on your server.",
+ },
+ style: {
+ description:
+ "Custom CSS properties to customize each individual card field. [Supported CSS properties](https://developer.paypal.com/docs/checkout/advanced/customize/card-field-style/#link-supportedcssproperties)",
+ control: { type: "object" },
+ ...{
+ ...COMPONENT_PROPS_CATEGORY,
+ type: { summary: "CSSProperties" },
+ },
+ },
+ inputEvents: {
+ control: false,
+ table: { category: "Props", type: { summary: "InputEvents?" } },
+ description:
+ "An object containing callbacks that will be applied to each input field.",
+ },
+ CardFieldsOnApproveData: {
+ control: false,
+ type: { required: false },
+ description: `{
+ orderID: string
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ InputEvents: {
+ control: false,
+ type: { required: false },
+ description: `{
+ onChange: (data: PayPalCardFieldsStateObject) => void
+ onBlur: (data: PayPalCardFieldsStateObject) => void
+ onFocus: (data: PayPalCardFieldsStateObject) => void
+ onInputSubmitRequest: (data: PayPalCardFieldsStateObject) => void
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObjectFields: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cardCvvField: PayPalCardFieldCardFieldData
+ cardNumberField: PayPalCardFieldCardFieldData
+ cardNameField: PayPalCardFieldCardFieldData
+ cardExpiryField: PayPalCardFieldCardFieldData
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cards: PayPalCardFieldsCardObject[]
+ emittedBy?: "name" | "number" | "cvv" | "expiry"
+ isFormValid: boolean
+ errors: PayPalCardFieldError[]
+ fields: PayPalCardFieldsStateObjectFields
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsCardObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: PayPalCardFieldSecurityCode
+ niceType: string
+ type: "american-express" | "diners-club" | "discover" | "jcb" | "maestro" | "mastercard" | "unionpay" | "visa" | "elo" | "hiper" | "hipercard"
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldError: {
+ control: false,
+ type: { required: false },
+ description: `"INELIGIBLE_CARD_VENDOR" | "INVALID_NAME" | "INVALID_NUMBER" | "INVALID_EXPIRY" | "INVALID_CVV"
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldSecurityCode: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: string
+ size: number
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldCardFieldData: {
+ control: false,
+ type: { required: false },
+ description: `{
+ isFocused: boolean
+ isEmpty: boolean
+ isValid: boolean
+ isPotentiallyValid: boolean
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ },
+};
+
+export const Default: FC = () => {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ action(CREATE_ORDER)("Start creating the order in custom endpoint");
+ return fetch(CREATE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ action(CREATE_ORDER)(order);
+ return order.id;
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ action(`Received ${ORDER_ID}`)(data.orderID);
+ action(CAPTURE_ORDER)(
+ `Sending ${ORDER_ID} to custom endpoint to capture the payment information`
+ );
+ fetch(CAPTURE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ action(CAPTURE_ORDER)(data);
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+ {/* Custom client component to handle card fields submit */}
+
+
+
+ );
+};
+
+/********************
+ * OVERRIDE STORIES *
+ *******************/
+(Default as StoryFn).parameters = {
+ docs: {
+ container: ({ context }: { context: DocsContextProps }) => (
+
+ ),
+ },
+};
diff --git a/src/stories/payPalCardFields/usePayPalCardFields.stories.tsx b/src/stories/payPalCardFields/usePayPalCardFields.stories.tsx
new file mode 100644
index 00000000..46e553a1
--- /dev/null
+++ b/src/stories/payPalCardFields/usePayPalCardFields.stories.tsx
@@ -0,0 +1,305 @@
+import React, { useState } from "react";
+import { action } from "@storybook/addon-actions";
+
+import {
+ PayPalScriptProvider,
+ usePayPalCardFields,
+ PayPalCardFieldsProvider,
+ PayPalCardFieldsForm,
+} from "../../index";
+import {
+ getOptionsFromQueryString,
+ generateRandomString,
+ CREATE_ORDER_URL,
+ CAPTURE_ORDER_URL,
+} from "../utils";
+import { COMPONENT_TYPES, ORDER_ID, ERROR } from "../constants";
+import { getFormCode } from "./code";
+import DocPageStructure from "../components/DocPageStructure";
+
+import type { FC } from "react";
+import type {
+ CardFieldsOnApproveData,
+ PayPalScriptOptions,
+} from "@paypal/paypal-js";
+import type { StoryFn } from "@storybook/react";
+import type { DocsContextProps } from "@storybook/addon-docs";
+
+const uid = generateRandomString();
+const scriptProviderOptions: PayPalScriptOptions = {
+ clientId:
+ "AduyjUJ0A7urUcWtGCTjanhRBSzOSn9_GKUzxWDnf51YaV1eZNA0ZAFhebIV_Eq-daemeI7dH05KjLWm",
+ components: "card-fields",
+ ...getOptionsFromQueryString(),
+};
+const CREATE_ORDER = "createOrder";
+const SUBMIT_FORM = "submitForm";
+const CAPTURE_ORDER = "captureOrder";
+
+const description = `
+The \`usePayPalCardFields\` custom hook provides access to the state managed by \`\`.
+`;
+
+/**
+ * Functional component to submit the hosted fields form
+ */
+
+const SubmitPayment: React.FC<{
+ isPaying: boolean;
+ setIsPaying: React.Dispatch>;
+}> = ({ isPaying, setIsPaying }) => {
+ const { cardFieldsForm } = usePayPalCardFields();
+
+ const handleClick = async () => {
+ if (!cardFieldsForm) {
+ const childErrorMessage =
+ "Unable to find any child components in the ";
+
+ action(ERROR)(childErrorMessage);
+ throw new Error(childErrorMessage);
+ }
+ const formState = await cardFieldsForm.getState();
+
+ if (!formState.isFormValid) {
+ return alert("The payment form is invalid");
+ }
+ action(SUBMIT_FORM)("Form is valid and submitted");
+ setIsPaying(true);
+
+ cardFieldsForm.submit().catch((err) => {
+ action(ERROR)(err.message);
+ setIsPaying(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default {
+ title: "PayPal/PayPalCardFields/Custom Hooks",
+ // component: usePayPalCardFields,
+ parameters: {
+ controls: { expanded: true, sort: "requiredFirst" },
+ docs: {
+ source: { language: "tsx" },
+ description: {
+ component: description,
+ },
+ },
+ },
+ argTypes: {
+ cardFieldsForm: {
+ control: false,
+ table: {
+ category: "State",
+ type: { summary: "PayPalCardFieldsComponent" },
+ },
+ description:
+ "Refers to the CardFields instance generated by the <PayPalCardFieldsProvider />.",
+ },
+ fields: {
+ control: false,
+ table: {
+ category: "State",
+ type: {
+ summary:
+ "Record",
+ },
+ },
+ description:
+ "An object containing the reference for each field rendered.",
+ },
+ FieldComponentName: {
+ control: false,
+ type: { required: false },
+ description: `"CVVField" | "ExpiryField" | "NumberField" | "NameField"
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsIndividualField: {
+ control: false,
+ type: { required: false },
+ description: `{
+ render: (container: string | HTMLElement) => Promise<void>
+ addClass: (className: string) => Promise<void>
+ clear: () => void;
+ focus: () => void;
+ removeAtrribute: (name: "aria-invalid" | "aria-required" | "disabled" | "placeholder") => Promise<void>
+ removeClass: (className: string) => Promise<void>
+ setAttribute: (name: string, value: string) => Promise<void>
+ setMessage: (message: string) => void
+ close: () => Promise<void>
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsComponent: {
+ control: false,
+ type: { required: false },
+ description: `{
+ getState: () => Promise<PayPalCardFieldsStateObject>
+ isEligible: () => boolean
+ submit: () => Promise<void>
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObjectFields: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cardCvvField: PayPalCardFieldCardFieldData
+ cardNumberField: PayPalCardFieldCardFieldData
+ cardNameField: PayPalCardFieldCardFieldData
+ cardExpiryField: PayPalCardFieldCardFieldData
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsStateObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ cards: PayPalCardFieldsCardObject[]
+ emittedBy?: "name" | "number" | "cvv" | "expiry"
+ isFormValid: boolean
+ errors: PayPalCardFieldError[]
+ fields: PayPalCardFieldsStateObjectFields
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldsCardObject: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: PayPalCardFieldSecurityCode
+ niceType: string
+ type: "american-express" | "diners-club" | "discover" | "jcb" | "maestro" | "mastercard" | "unionpay" | "visa" | "elo" | "hiper" | "hipercard"
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldError: {
+ control: false,
+ type: { required: false },
+ description: `"INELIGIBLE_CARD_VENDOR" | "INVALID_NAME" | "INVALID_NUMBER" | "INVALID_EXPIRY" | "INVALID_CVV"
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldSecurityCode: {
+ control: false,
+ type: { required: false },
+ description: `{
+ code: string
+ size: number
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ PayPalCardFieldCardFieldData: {
+ control: false,
+ type: { required: false },
+ description: `{
+ isFocused: boolean
+ isEmpty: boolean
+ isValid: boolean
+ isPotentiallyValid: boolean
+ }
+ `,
+ table: { category: COMPONENT_TYPES },
+ },
+ },
+};
+
+export const Default: FC = () => {
+ const [isPaying, setIsPaying] = useState(false);
+ async function createOrder() {
+ action(CREATE_ORDER)("Start creating the order in custom endpoint");
+ return fetch(CREATE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ cart: [
+ {
+ sku: "1blwyeo8",
+ quantity: 2,
+ },
+ ],
+ }),
+ })
+ .then((response) => response.json())
+ .then((order) => {
+ action(CREATE_ORDER)(order);
+ return order.id;
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ console.error(err);
+ });
+ }
+
+ function onApprove(data: CardFieldsOnApproveData) {
+ action(`Received ${ORDER_ID}`)(data.orderID);
+ action(CAPTURE_ORDER)(
+ `Sending ${ORDER_ID} to custom endpoint to capture the payment information`
+ );
+ fetch(CAPTURE_ORDER_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ orderID: data.orderID }),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ action(CAPTURE_ORDER)(data);
+ setIsPaying(false);
+ })
+ .catch((err) => {
+ action(ERROR)(err.message);
+ });
+ }
+ return (
+
+ {
+ console.log(err);
+ }}
+ >
+
+ {/* Custom client component to handle card fields submit */}
+
+
+
+ );
+};
+
+/********************
+ * OVERRIDE STORIES *
+ *******************/
+(Default as StoryFn).parameters = {
+ docs: {
+ container: ({ context }: { context: DocsContextProps }) => (
+
+ ),
+ },
+};