From 73676e6081d24e33485aad1268a7f6b45953bc4f Mon Sep 17 00:00:00 2001 From: Mervin Choun Date: Thu, 25 Apr 2024 17:33:52 -0400 Subject: [PATCH] fix: skeleton for individual stories --- src/stories/payPalCardFields/code.ts | 235 +++++++++++++ .../payPalCVVField.stories.tsx | 313 ++++++++++++++++++ .../payPalCardFieldsForm.stories.tsx | 0 .../payPalExpiryField.stories.tsx | 313 ++++++++++++++++++ .../payPalNameField.stories.tsx | 313 ++++++++++++++++++ .../payPalNumberField.stories.tsx | 313 ++++++++++++++++++ src/stories/payPalCardFieldsForm/code.ts | 82 ----- 7 files changed, 1487 insertions(+), 82 deletions(-) create mode 100644 src/stories/payPalCardFields/code.ts create mode 100644 src/stories/payPalCardFields/payPalCVVField.stories.tsx rename src/stories/{payPalCardFieldsForm => payPalCardFields}/payPalCardFieldsForm.stories.tsx (100%) create mode 100644 src/stories/payPalCardFields/payPalExpiryField.stories.tsx create mode 100644 src/stories/payPalCardFields/payPalNameField.stories.tsx create mode 100644 src/stories/payPalCardFields/payPalNumberField.stories.tsx delete mode 100644 src/stories/payPalCardFieldsForm/code.ts diff --git a/src/stories/payPalCardFields/code.ts b/src/stories/payPalCardFields/code.ts new file mode 100644 index 00000000..cf59795d --- /dev/null +++ b/src/stories/payPalCardFields/code.ts @@ -0,0 +1,235 @@ +import { CREATE_ORDER_URL, CAPTURE_ORDER_URL } from "../utils"; + +import type { Args } from "@storybook/addons/dist/ts3.9/types"; + +export const getFormCode = (args: Args): 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 } = 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 = (args: Args): string => { + return ` + import React, { useState } from "react"; + import type { CardFieldsOnApproveData } from "@paypal/paypal-js"; + + import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + //four different components and rendering them + } 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 } = 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 ( + + ); + }; + + `; +}; diff --git a/src/stories/payPalCardFields/payPalCVVField.stories.tsx b/src/stories/payPalCardFields/payPalCVVField.stories.tsx new file mode 100644 index 00000000..214f2687 --- /dev/null +++ b/src/stories/payPalCardFields/payPalCVVField.stories.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { action } from "@storybook/addon-actions"; + +import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + PayPalCVVField, +} from "../../index"; +import { + getOptionsFromQueryString, + generateRandomString, + getClientToken, + FLY_SERVER, + CREATE_ORDER_URL, + CAPTURE_ORDER_URL, +} from "../utils"; +import { + COMPONENT_PROPS_CATEGORY, + COMPONENT_TYPES, + ORDER_ID, + ERROR, +} from "../constants"; + +import type { FC, ReactElement } from "react"; +import type { + CardFieldsOnApproveData, + PayPalScriptOptions, +} from "@paypal/paypal-js"; +import { StoryFn } from "@storybook/react"; +import { DocsContextProps } from "@storybook/addon-docs"; +import DocPageStructure from "../components/DocPageStructure"; +import { getFormCode } from "./code"; + +const uid = generateRandomString(); +const TOKEN_URL = `${FLY_SERVER}/api/paypal/generate-client-token`; +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/Individual", + component: PayPalCVVField, + 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", + }, + }, + style: { + description: + "Custom CSS properties to customize each individual card field", + control: { type: "object" }, + ...{ + ...COMPONENT_PROPS_CATEGORY, + type: { summary: "CSSProperties" }, + }, + }, + inputEvents: { + control: false, + table: { category: "Props", type: { summary: "InputEvents?" } }, + }, + 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 }, + }, + }, + decorators: [ + (Story: FC): ReactElement => { + // Workaround to render the story after got the client token, + // The new experimental loaders doesn't work in Docs views + const [clientToken, setClientToken] = useState(null); + + useEffect(() => { + (async () => { + setClientToken(await getClientToken(TOKEN_URL)); + })(); + }, []); + + return ( +
+ {clientToken && ( + <> + + + + + )} +
+ ); + }, + ], +}; + +export const CVVField: 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 * + *******************/ +(CVVField as StoryFn).parameters = { + docs: { + container: ({ context }: { context: DocsContextProps }) => ( + + ), + }, +}; diff --git a/src/stories/payPalCardFieldsForm/payPalCardFieldsForm.stories.tsx b/src/stories/payPalCardFields/payPalCardFieldsForm.stories.tsx similarity index 100% rename from src/stories/payPalCardFieldsForm/payPalCardFieldsForm.stories.tsx rename to src/stories/payPalCardFields/payPalCardFieldsForm.stories.tsx diff --git a/src/stories/payPalCardFields/payPalExpiryField.stories.tsx b/src/stories/payPalCardFields/payPalExpiryField.stories.tsx new file mode 100644 index 00000000..9163792d --- /dev/null +++ b/src/stories/payPalCardFields/payPalExpiryField.stories.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { action } from "@storybook/addon-actions"; + +import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + PayPalExpiryField, +} from "../../index"; +import { + getOptionsFromQueryString, + generateRandomString, + getClientToken, + FLY_SERVER, + CREATE_ORDER_URL, + CAPTURE_ORDER_URL, +} from "../utils"; +import { + COMPONENT_PROPS_CATEGORY, + COMPONENT_TYPES, + ORDER_ID, + ERROR, +} from "../constants"; + +import type { FC, ReactElement } from "react"; +import type { + CardFieldsOnApproveData, + PayPalScriptOptions, +} from "@paypal/paypal-js"; +import { StoryFn } from "@storybook/react"; +import { DocsContextProps } from "@storybook/addon-docs"; +import DocPageStructure from "../components/DocPageStructure"; +import { getFormCode } from "./code"; + +const uid = generateRandomString(); +const TOKEN_URL = `${FLY_SERVER}/api/paypal/generate-client-token`; +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/Individual", + component: PayPalExpiryField, + 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", + }, + }, + style: { + description: + "Custom CSS properties to customize each individual card field", + control: { type: "object" }, + ...{ + ...COMPONENT_PROPS_CATEGORY, + type: { summary: "CSSProperties" }, + }, + }, + inputEvents: { + control: false, + table: { category: "Props", type: { summary: "InputEvents?" } }, + }, + 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 }, + }, + }, + decorators: [ + (Story: FC): ReactElement => { + // Workaround to render the story after got the client token, + // The new experimental loaders doesn't work in Docs views + const [clientToken, setClientToken] = useState(null); + + useEffect(() => { + (async () => { + setClientToken(await getClientToken(TOKEN_URL)); + })(); + }, []); + + return ( +
+ {clientToken && ( + <> + + + + + )} +
+ ); + }, + ], +}; + +export const ExpiryField: 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 * + *******************/ +(ExpiryField as StoryFn).parameters = { + docs: { + container: ({ context }: { context: DocsContextProps }) => ( + + ), + }, +}; diff --git a/src/stories/payPalCardFields/payPalNameField.stories.tsx b/src/stories/payPalCardFields/payPalNameField.stories.tsx new file mode 100644 index 00000000..a63789b2 --- /dev/null +++ b/src/stories/payPalCardFields/payPalNameField.stories.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { action } from "@storybook/addon-actions"; + +import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + PayPalNameField, +} from "../../index"; +import { + getOptionsFromQueryString, + generateRandomString, + getClientToken, + FLY_SERVER, + CREATE_ORDER_URL, + CAPTURE_ORDER_URL, +} from "../utils"; +import { + COMPONENT_PROPS_CATEGORY, + COMPONENT_TYPES, + ORDER_ID, + ERROR, +} from "../constants"; + +import type { FC, ReactElement } from "react"; +import type { + CardFieldsOnApproveData, + PayPalScriptOptions, +} from "@paypal/paypal-js"; +import { StoryFn } from "@storybook/react"; +import { DocsContextProps } from "@storybook/addon-docs"; +import DocPageStructure from "../components/DocPageStructure"; +import { getFormCode } from "./code"; + +const uid = generateRandomString(); +const TOKEN_URL = `${FLY_SERVER}/api/paypal/generate-client-token`; +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/Individual", + component: PayPalNameField, + 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", + }, + }, + style: { + description: + "Custom CSS properties to customize each individual card field", + control: { type: "object" }, + ...{ + ...COMPONENT_PROPS_CATEGORY, + type: { summary: "CSSProperties" }, + }, + }, + inputEvents: { + control: false, + table: { category: "Props", type: { summary: "InputEvents?" } }, + }, + 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 }, + }, + }, + decorators: [ + (Story: FC): ReactElement => { + // Workaround to render the story after got the client token, + // The new experimental loaders doesn't work in Docs views + const [clientToken, setClientToken] = useState(null); + + useEffect(() => { + (async () => { + setClientToken(await getClientToken(TOKEN_URL)); + })(); + }, []); + + return ( +
+ {clientToken && ( + <> + + + + + )} +
+ ); + }, + ], +}; + +export const NameField: 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 * + *******************/ +(NameField as StoryFn).parameters = { + docs: { + container: ({ context }: { context: DocsContextProps }) => ( + + ), + }, +}; diff --git a/src/stories/payPalCardFields/payPalNumberField.stories.tsx b/src/stories/payPalCardFields/payPalNumberField.stories.tsx new file mode 100644 index 00000000..97a63a1c --- /dev/null +++ b/src/stories/payPalCardFields/payPalNumberField.stories.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { action } from "@storybook/addon-actions"; + +import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + PayPalNumberField, +} from "../../index"; +import { + getOptionsFromQueryString, + generateRandomString, + getClientToken, + FLY_SERVER, + CREATE_ORDER_URL, + CAPTURE_ORDER_URL, +} from "../utils"; +import { + COMPONENT_PROPS_CATEGORY, + COMPONENT_TYPES, + ORDER_ID, + ERROR, +} from "../constants"; + +import type { FC, ReactElement } from "react"; +import type { + CardFieldsOnApproveData, + PayPalScriptOptions, +} from "@paypal/paypal-js"; +import { StoryFn } from "@storybook/react"; +import { DocsContextProps } from "@storybook/addon-docs"; +import DocPageStructure from "../components/DocPageStructure"; +import { getFormCode } from "./code"; + +const uid = generateRandomString(); +const TOKEN_URL = `${FLY_SERVER}/api/paypal/generate-client-token`; +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/Individual", + component: PayPalNumberField, + 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", + }, + }, + style: { + description: + "Custom CSS properties to customize each individual card field", + control: { type: "object" }, + ...{ + ...COMPONENT_PROPS_CATEGORY, + type: { summary: "CSSProperties" }, + }, + }, + inputEvents: { + control: false, + table: { category: "Props", type: { summary: "InputEvents?" } }, + }, + 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 }, + }, + }, + decorators: [ + (Story: FC): ReactElement => { + // Workaround to render the story after got the client token, + // The new experimental loaders doesn't work in Docs views + const [clientToken, setClientToken] = useState(null); + + useEffect(() => { + (async () => { + setClientToken(await getClientToken(TOKEN_URL)); + })(); + }, []); + + return ( +
+ {clientToken && ( + <> + + + + + )} +
+ ); + }, + ], +}; + +export const NumberField: 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 * + *******************/ +(NumberField as StoryFn).parameters = { + docs: { + container: ({ context }: { context: DocsContextProps }) => ( + + ), + }, +}; diff --git a/src/stories/payPalCardFieldsForm/code.ts b/src/stories/payPalCardFieldsForm/code.ts deleted file mode 100644 index b2893d2b..00000000 --- a/src/stories/payPalCardFieldsForm/code.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { CREATE_ORDER_URL, CAPTURE_ORDER_URL } from "../utils"; - -import type { Args } from "@storybook/addons/dist/ts3.9/types"; - -export const getFormCode = (args: Args): string => { - return ` - import React, { useState } from "react"; - import type { CardFieldsOnApproveData } from "@paypal/paypal-js"; - - import { - PayPalScriptProvider, - // usePayPalCardFields, - PayPalCardFieldsProvider, - // PayPalCardFieldsForm, - } from "src/index.ts"; - - 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); - }); - } - - export default function App(): JSX.Element { - const [isPaying, setIsPaying] = useState(false); - return ( - - { - console.log(err); - }} - > - - - ); - } - - - `; -};