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 }) => ( + + ), + }, +};