From a8d49ba671c5f86b051abb17f2e4c56b7bb79391 Mon Sep 17 00:00:00 2001 From: ian Date: Mon, 27 Jan 2025 16:25:27 -0500 Subject: [PATCH 1/5] Updates for v1.2.6 - Added docstrings to components and functions - Exposed PayloadForm.submit function - Forwarded ref for higher order form components (PaymentForm and PaymentMethodForm) - Fixed issue with payment and paymentMethod props not updating on rerender --- package-lock.json | 34 +- package.json | 2 +- src/mappings.js | 67 ++++ src/payload-react.js | 372 +++++++++++++----- src/utils.js | 46 +++ .../__snapshots__/payload-react.test.js.snap | 12 + tests/payload-react.test.js | 93 ++++- 7 files changed, 508 insertions(+), 118 deletions(-) create mode 100644 src/mappings.js diff --git a/package-lock.json b/package-lock.json index 8540245..6709de5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "payload-react", - "version": "1.2.4", + "version": "1.2.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "payload-react", - "version": "1.2.4", + "version": "1.2.6", "license": "MIT", "dependencies": { "prop-types": "^15.8.1", @@ -4259,10 +4259,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8030,12 +8031,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -13579,9 +13581,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -16310,12 +16312,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, diff --git a/package.json b/package.json index 40128b4..2ceaae1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload-react", - "version": "1.2.5", + "version": "1.2.6", "description": "A simple React wrapper around Payload.js. See https://docs.payload.com for more information.", "main": "dist/payload-react.js", "module": "dist/payload-react.js", diff --git a/src/mappings.js b/src/mappings.js new file mode 100644 index 0000000..96e53fc --- /dev/null +++ b/src/mappings.js @@ -0,0 +1,67 @@ +export const sensitiveFields = { + account_number: true, + routing_number: true, + card_code: true, + cvc: true, + card_number: true, + expiry: true, + card: true, +} + +export const formParamsMap = { + autosubmit: 'autoSubmit', + styles: 'styles', + payment: 'payment', + payment_method: 'paymentMethod', + preventDefaultOnSubmit: 'preventDefaultOnSubmit', + preventSubmitOnEnter: 'preventSubmitOnEnter', +} + +export const formEventsMap = { + processing: 'onProcessing', + processed: 'onProcessed', + authorized: 'onAuthorized', + error: 'onError', + declined: 'onDeclined', + created: 'onCreated', + success: 'onSuccess', +} + +export const inputPropsMap = { + 'disable-paste': 'disablePaste', +} + +export const inputEventsMap = { + invalid: 'onInvalid', + valid: 'onValid', + focus: 'onFocus', + blur: 'onBlur', + change: 'onChange', +} + +export const processingFormEventsMap = { + success: 'onSuccess', + account_created: 'onAccountCreated', + loaded: 'onLoaded', + closed: 'onClosed', +} + +export const processingFormAttributeMap = { + form: 'form', + legal_entity_id: 'legalEntityId', +} + +export const checkoutEventsMap = { + processed: 'onProcessed', + authorized: 'onAuthorized', + declined: 'onDeclined', + success: 'onSuccess', + loaded: 'onLoaded', + closed: 'onClosed', +} + +export const checkoutAttributeMap = { + form: 'form', + autosubmit: 'autoSubmit', + amount: 'amount', +} diff --git a/src/payload-react.js b/src/payload-react.js index eae78ee..64bbd15 100644 --- a/src/payload-react.js +++ b/src/payload-react.js @@ -1,110 +1,59 @@ import PropTypes from 'prop-types' -import React, { useEffect, useRef } from 'react' - -import { getPayload } from './utils.js' +import React, { forwardRef } from 'react' + +import { + checkoutAttributeMap, + checkoutEventsMap, + formEventsMap, + formParamsMap, + inputEventsMap, + inputPropsMap, + processingFormAttributeMap, + processingFormEventsMap, + sensitiveFields, +} from './mappings.js' +import { cacheCls, getPayload, getPropAttrs, invertObject } from './utils.js' const PayloadFormContext = React.createContext(null) -const sensitiveFields = { - account_number: true, - routing_number: true, - card_code: true, - cvc: true, - card_number: true, - expiry: true, - card: true, -} - -const formParamsMap = { - autosubmit: 'autoSubmit', - styles: 'styles', - payment: 'payment', - payment_method: 'paymentMethod', - preventDefaultOnSubmit: 'preventDefaultOnSubmit', - preventSubmitOnEnter: 'preventSubmitOnEnter', -} - -const formEventsMap = { - processing: 'onProcessing', - processed: 'onProcessed', - authorized: 'onAuthorized', - error: 'onError', - declined: 'onDeclined', - created: 'onCreated', - success: 'onSuccess', -} - -const inputPropsMap = { - 'disable-paste': 'disablePaste', -} - -const inputEventsMap = { - invalid: 'onInvalid', - valid: 'onValid', - focus: 'onFocus', - blur: 'onBlur', - change: 'onChange', -} - -const processingFormEventsMap = { - success: 'onSuccess', - account_created: 'onAccountCreated', - loaded: 'onLoaded', - closed: 'onClosed', -} - -const processingFormAttributeMap = { - form: 'form', - legal_entity_id: 'legalEntityId', -} - -const checkoutEventsMap = { - processed: 'onProcessed', - authorized: 'onAuthorized', - declined: 'onDeclined', - success: 'onSuccess', - loaded: 'onLoaded', - closed: 'onClosed', -} - -const checkoutAttributeMap = { - form: 'form', - autosubmit: 'autoSubmit', - amount: 'amount', -} - -function getPropAttrs(props, ignore) { - const attrs = {} - for (const key in props) { - if (key == 'children') continue - if (ignore && ignore.includes(key)) continue - attrs[key] = props[key] - } - return attrs -} - -const __cls_cache = {} - -function cacheCls(name, cls) { - if (!(name in __cls_cache)) __cls_cache[name] = cls - return __cls_cache[name] -} - +/** + * Represents a form input component that handles sensitive data. + * + * @class PayloadInput + * @extends React.Component + */ export class PayloadInput extends React.Component { + /** + * The context type for the component. + * + * @type {PayloadFormContext} + */ static contextType = PayloadFormContext + /** + * Creates an instance of PayloadInput. + * @param {Object} props - The props for the component. + */ constructor(props) { super(props) this.props = props this.inputRef = React.createRef() } + /** + * Checks if the field is sensitive. + * + * @returns {boolean} - True if the field is sensitive, false otherwise. + */ isSensitiveField() { return sensitiveFields[ this.props.attr || this._pl_input || this.props['pl-input'] ] } + /** + * Adds event listeners when the component mounts. + */ componentDidMount() { if (!this.isSensitiveField()) return @@ -116,6 +65,9 @@ export class PayloadInput extends React.Component { }) } + /** + * Removes event listeners when the component unmounts. + */ componentWillUnmount() { if (!this.isSensitiveField()) return @@ -124,6 +76,11 @@ export class PayloadInput extends React.Component { }) } + /** + * Renders an input element or a div element based on the props and sensitivity of the field. + * + * @returns {JSX.Element} - Returns a JSX element representing an input or div element. + */ render() { const attrs = getPropAttrs( this.props, @@ -155,6 +112,18 @@ export class PayloadInput extends React.Component { } } +/** + * Represents a form component that integrates with the Payload.js library for handling form submissions. + * + * @param {Object} props - The properties passed to the component. + * @param {string} props.clientToken - The client token required for initializing the third-party library. + * @param {Object} props.children - The child components to be rendered within the form. + * @param {Function} props.Payload - (Development Use Only) The third-party library for handling form submissions. + * + * @returns {JSX.Element} - A form component integrated with the third-party library. + * + * @throws {Error} - If the client token is missing or if there is an issue initializing the third-party library. + */ export class PayloadForm extends React.Component { constructor(props) { super(props) @@ -166,6 +135,15 @@ export class PayloadForm extends React.Component { this.formRef = React.createRef() } + /** + * Asynchronously initializes the component by checking for the presence of a client token and Payload.js. + * If the client token is not provided, the function will return early. + * If Payload.js is not already stored in the component's state, it will attempt to retrieve it from the window object. + * If Payload.js is successfully retrieved, it will be stored in the component's state. + * Finally, the function will call the initializePayload method to initialize the Payload.js component. + * + * @returns {void} + */ async componentDidMount() { if (!this.props.clientToken) { return @@ -179,13 +157,32 @@ export class PayloadForm extends React.Component { } else this.initalizePayload() } + /** + * componentDidUpdate - Lifecycle method that is invoked immediately after updating occurs. + * + * @param {object} prevProps - The previous props before the update. + * @param {object} prevState - The previous state before the update. + * @param {object} snapshot - The snapshot value returned by getSnapshotBeforeUpdate. + * + * @returns {void} + * + * This method checks if the previous state did not have a Payload, but the current state does have a Payload. + * If this condition is met, it calls the initializePayload method to initialize the Payload.js component. + */ componentDidUpdate(prevProps, prevState, snapshot) { if (!prevState.Payload && this.state.Payload) { // Payload is set in our state we can initialize it this.initalizePayload() + } else { + this.updatePayload() } } + /** + * Initializes Payload.js for the form based on the client token and form parameters. + * + * @returns {void} + */ initalizePayload() { this.state.Payload(this.props.clientToken) @@ -215,6 +212,21 @@ export class PayloadForm extends React.Component { }) } + updatePayload() { + if (this.props.payment) this.pl_form.params.payment = this.props.payment + if (this.props.paymentMethod) + this.pl_form.params[invertObject(formParamsMap).paymentMethod] = + this.props.paymentMethod + } + + /** + * Adds a listener for a specific event. + * + * @param {string} evt - The event to listen for. + * @param {Object} ref - The reference to the object that the listener is attached to. + * @param {function} cb - The callback function to be executed when the event is triggered. + * @returns {void} + */ addListener(evt, ref, cb) { const listeners = this.state.listeners if (!(evt in listeners)) listeners[evt] = [] @@ -222,9 +234,17 @@ export class PayloadForm extends React.Component { this.setState({ listeners }) } + /** + * Removes a listener from the event listeners list. + * + * @param {string} evt - The event name to remove the listener from. + * @param {Object} ref - The reference to the listener function to be removed. + * + * @returns {void} + */ removeListener(evt, ref) { const listeners = this.state.listeners - const index = listeners[evt]?.findIndex(([r, cb]) => r === ref) ?? -1 + const index = listeners[evt]?.findIndex(([r]) => r === ref) ?? -1 if (index != -1) { listeners[evt].splice(index, 1) @@ -232,6 +252,20 @@ export class PayloadForm extends React.Component { } } + /** + * Submits the form using the Payload.js Form.submit method. + * + * This method triggers the submission of the form using the Payload.js library's Form.submit method. + * It is important to note that this method does not handle the form submission logic itself, + * but rather delegates it to the Payload.js library. + * + * @returns {Promise} A Promise that resolves when the form submission is successful. + * @throws {Error} If the form submission fails or encounters an error. + */ + submit() { + return this.pl_form.submit() + } + render() { const attrs = getPropAttrs(this.props, [ ...Object.values(formParamsMap), @@ -241,7 +275,7 @@ export class PayloadForm extends React.Component { 'Payload', ]) - if (this._pl_form) attrs['pl-form'] = this._pl_form + if (this._pl_form_type) attrs['pl-form'] = this._pl_form_type return ( { +/** + * A higher order component that wraps for processing payments. + * + * @param {Object} children - The child components to be rendered within the PaymentForm. + * @param {Object} props - Additional props to be passed to the PaymentForm component. + * @param {Object} ref - A reference to the PaymentForm component. + * + * @returns {JSX.Element} - Returns a JSX element representing the PaymentForm component. + */ +export const PaymentForm = forwardRef(({ children, ...props }, ref) => { return ( - + {children} ) -} - -export const PaymentMethodForm = ({ children, ...props }) => { +}) + +/** + * A higher order component that wraps for collecting payment method information. + * + * @param {Object} children - The child components to be rendered within the form. + * @param {Object} props - Additional props to be passed to the form component. + * @param {Object} ref - A reference to the form component. + * @returns {JSX.Element} - The rendered form component with the provided children. + */ +export const PaymentMethodForm = forwardRef(({ children, ...props }, ref) => { return ( - + {children} ) -} - +}) + +/** + * Creates a card input component. + * + * @param {Object} props - The properties to be passed to the component. + * @returns {JSX.Element} - A card input component. + */ export const Card = (props) => { return } +/** + * Creates a card number input component. + * + * @param {Object} props - The properties to be passed to the component. + * @returns {JSX.Element} - A card number input component. + */ export const CardNumber = (props) => { return } +/** + * Creates an expiry input component. + * + * @param {Object} props - The properties to be passed to the component. + * @returns {JSX.Element} - An expiry input component. + */ export const Expiry = (props) => { return } +/** + * Creates a card code input component. + * + * @param {Object} props - The properties to be passed to the component. + * @returns {JSX.Element} - A card code input component. + */ export const CardCode = (props) => { return } +/** + * Renders an input field for routing number. + * + * @param {Object} props - The props to be passed to the component. + * @returns {JSX.Element} - A JSX element representing the input field for routing number. + */ export const RoutingNumber = (props) => { return } +/** + * Renders an input field for account number. + * + * @param {Object} props - The props to be passed to the component. + * @returns {JSX.Element} - A JSX element representing the input field for account number. + */ export const AccountNumber = (props) => { return } +/** + * ProcessingAccountForm component for rendering a processing account form + * + * This component is responsible for rendering a processing account form using the provided client token. + * It initializes the Payload.js object, sets up event listeners, and renders the form using the provided props. + * + * @param {Object} props - The props object containing the Payload object and client token + * @param {string} props.clientToken - The client token used for authentication + * @param {Object} props.Payload - (Development Use Only) The Payload object used for processing account form + * + * @returns {JSX.Element} - The rendered processing account form component + * + * @throws {Error} - If the client token is not provided + */ export class ProcessingAccountForm extends React.Component { constructor(props) { super(props) @@ -311,6 +412,16 @@ export class ProcessingAccountForm extends React.Component { .concat(['Payload', 'clientToken']) } + /** + * Asynchronously initializes the component by checking for the presence of a client token and payload. + * If the client token is not present, the function returns early. + * If Payload.js is not present in the component state, it checks if Payload.js is available in the global window object. + * If Payload.js is not available in the global window object, it fetches Payload.js using the getPayload function. + * Once Payload.js is available, it updates the component state with Payload object. + * If Payload.js is already present in the component state, it calls the initializePayload function. + * + * @returns {void} + */ async componentDidMount() { if (!this.props.clientToken) { return @@ -326,6 +437,19 @@ export class ProcessingAccountForm extends React.Component { } } + /** + * componentDidUpdate - Lifecycle method that is called after a component has been updated. + * + * @param {object} prevProps - The previous props of the component. + * @param {object} prevState - The previous state of the component. + * @param {object} snapshot - The snapshot value returned by getSnapshotBeforeUpdate. + * + * @returns {void} + * + * This method checks if the previous state did not have a 'Payload' property but the current state does. + * If this condition is met, initialize Payload and create a new 'ProcessingAccount' object. + * It then maps certain props from the component to the 'ProcessingAccount' object and sets up event listeners. + */ componentDidUpdate(prevProps, prevState, snapshot) { if (!prevState.Payload && this.state.Payload) { // Payload is set in our state we can initialize it @@ -363,6 +487,12 @@ export class ProcessingAccountForm extends React.Component { } } +/** + * Opens the processing account form using the provided props. + * + * @param {Object} props - The props object containing the necessary information to populate the form. + * @returns {Promise} - A Promise that resolves to the processing account object. + */ export const openProcessingAccountForm = async (props) => { await getPayload() @@ -384,6 +514,17 @@ export const openProcessingAccountForm = async (props) => { return processingAccount } +/** + * Checkout component for handling payment processing + * + * @param {Object} props - The props for the Checkout component + * @param {string} props.clientToken - The client token for authentication + * @param {Object} props.Payload - (Development Use Only) The Payload object for payment processing + * + * @returns {JSX.Element} - The rendered Checkout component + * + * @throws {Error} - If clientToken is not provided + */ export class Checkout extends React.Component { constructor(props) { super(props) @@ -398,6 +539,16 @@ export class Checkout extends React.Component { .concat(['Payload', 'clientToken']) } + /** + * Asynchronously initializes the component by checking for the presence of a client token and payload. + * If the client token is not present, the function returns early. + * If Payload.js is not present in the component state, it checks if Payload.js is available in the global window object. + * If Payload.js is not available in the global window object, it fetches Payload.js using the getPayload function. + * Once Payload.js is available, it updates the component state with the Payload object. + * If Payload.js is already present in the component state, it calls the initializePayload function. + * + * @returns {void} + */ async componentDidMount() { if (!this.props.clientToken) { return @@ -413,6 +564,19 @@ export class Checkout extends React.Component { } } + /** + * componentDidUpdate - Lifecycle method that is called after a component has been updated. + * + * @param {object} prevProps - The previous props of the component. + * @param {object} prevState - The previous state of the component. + * @param {object} snapshot - The snapshot value returned by getSnapshotBeforeUpdate. + * + * @returns {void} + * + * This method checks if the previous state did not have a 'Payload' property but the current state does. + * If this condition is met, it initializes the 'Payload' and creates a new 'Checkout' instance using the 'Payload'. + * It then sets up event listeners for the 'Checkout' instance based on the 'checkoutEventsMap' and calls corresponding props functions. + */ componentDidUpdate(prevProps, prevState, snapshot) { if (!prevState.Payload && this.state.Payload) { // Payload is set in our state we can initialize it @@ -451,6 +615,13 @@ export class Checkout extends React.Component { } } +/** + * Opens Payload.js Checkout using the provided props and returns the checkout object. + * + * @param {Object} props - The props object containing clientToken and other checkout attributes. + * @returns {Object} - The checkout object created using the provided props. + * @throws {Error} - If there is an issue with getting Payload.js object or creating the checkout object. + */ export const openCheckout = async (props) => { await getPayload() @@ -477,6 +648,9 @@ PayloadForm.propTypes = { Payload: PropTypes.func, } +/** + * @deprecated Use PayloadForm and PayloadInput and their higher order components + */ const PayloadReact = { input: new Proxy( {}, @@ -524,7 +698,7 @@ const PayloadReact = { 'form.' + name, class extends PayloadForm { render() { - this._pl_form = name + this._pl_form_type = name return super.render() } } diff --git a/src/utils.js b/src/utils.js index c17e007..3d78269 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,15 @@ let loadingPromise = null +const __clsCache = {} + +/** + * Retrieves the Payload object asynchronously. + * + * This function checks if the Payload object is already available in the window. If not, it dynamically loads the Payload.js script from 'https://payload.com/Payload.js'. + * + * @returns {Promise} A Promise that resolves with the Payload object once it is available. + * @throws {Error} If there is an error loading the Payload.js script. + */ export function getPayload() { if (!loadingPromise) loadingPromise = new Promise((resolve, reject) => { @@ -25,3 +35,39 @@ export function getPayload() { return loadingPromise } + +/** + * Inverts the key-value pairs of an object. + * + * @param {Object} obj - The object to invert. + * @returns {Object} - The inverted object with keys and values swapped. + */ +export function invertObject(obj) { + return Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [v]: k }), {}) +} + +/** + * Extracts and returns a new object containing all properties of the input object, + * except for the 'children' property and any properties specified in the 'ignore' array. + * + * @param {Object} props - The input object containing properties to extract. + * @param {Array} ignore - An optional array of property names to ignore and not + * include in the returned object. + * @returns {Object} - A new object containing all properties of the input object, + * except for the 'children' property and any properties specified + * in the 'ignore' array. + */ +export function getPropAttrs(props, ignore) { + const attrs = {} + for (const key in props) { + if (key == 'children') continue + if (ignore && ignore.includes(key)) continue + attrs[key] = props[key] + } + return attrs +} + +export function cacheCls(name, cls) { + if (!(name in __clsCache)) __clsCache[name] = cls + return __clsCache[name] +} diff --git a/tests/__snapshots__/payload-react.test.js.snap b/tests/__snapshots__/payload-react.test.js.snap index cd02ac1..4f660b0 100644 --- a/tests/__snapshots__/payload-react.test.js.snap +++ b/tests/__snapshots__/payload-react.test.js.snap @@ -123,6 +123,18 @@ exports[`PayloadReact expect state to be updated with Payload when window.Payloa }, ], }, + "params": { + "form":
+ +
, + }, + "submit": [MockFunction], }, }, ], diff --git a/tests/payload-react.test.js b/tests/payload-react.test.js index 1523a1e..28c7003 100644 --- a/tests/payload-react.test.js +++ b/tests/payload-react.test.js @@ -9,6 +9,7 @@ import PayloadReact, { CardNumber, Checkout, Expiry, + PayloadForm, PayloadInput, PaymentForm, ProcessingAccountForm, @@ -31,8 +32,8 @@ const getReactEvtName = (evt) => function mockPayload() { const Payload = jest.fn() - Payload.Form = jest.fn().mockImplementation(() => { - return { on: jest.fn() } + Payload.Form = jest.fn().mockImplementation((opts) => { + return { on: jest.fn(), submit: jest.fn(), params: opts } }) Payload.ProcessingAccount = jest.fn().mockImplementation(() => { return { on: jest.fn() } @@ -494,6 +495,94 @@ describe('PayloadReact', () => { } ) + it('expect ref.current.submit to call Payload.js Form.submit', async () => { + const Payload = mockPayload() + + const getPayloadMock = jest + .spyOn(utils, 'getPayload') + .mockImplementation(() => { + global.Payload = Payload + return Promise.resolve() + }) + + const paymentFormRef = React.createRef() + + const Test = () => { + return ( + + + + + ) + } + + const form = mount() + + await waitFor(() => { + expect(Payload).toHaveBeenCalledWith('test_fake_token_1234567') + expect(form.find('#card-number').exists()).toBe(true) + }) + + expect(Payload.Form.mock.results[0].value.submit).not.toHaveBeenCalled() + + paymentFormRef.current.submit() + + expect(Payload.Form.mock.results[0].value.submit).toHaveBeenCalled() + }) + + it.each([ + ['payment', 'payment'], + ['paymentMethod', 'payment_method'], + ])( + 'expect prefill props to PayloadForm to update when rerendered', + async (propName, paramName) => { + const Payload = mockPayload() + + const getPayloadMock = jest + .spyOn(utils, 'getPayload') + .mockImplementation(() => { + global.Payload = Payload + return Promise.resolve() + }) + + let setObjectRef + + const Test = () => { + const [obj, setObject] = useState({}) + + setObjectRef = setObject + + return ( + + + + + ) + } + + const form = mount() + + await waitFor(() => { + expect(Payload).toHaveBeenCalledWith('test_fake_token_1234567') + expect(form.find('#card-number').exists()).toBe(true) + }) + + expect( + Payload.Form.mock.results[0].value.params[paramName] + ).toStrictEqual({}) + + setObjectRef({ changed: true }) + + expect( + Payload.Form.mock.results[0].value.params[paramName] + ).toStrictEqual({ + changed: true, + }) + } + ) + it('expect ProcessingAccountForm event props to fire', async () => { const Payload = mockPayload() From 7dbff3421743d2591f4fa4e04e491e79de5cd1ac Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 28 Feb 2025 14:15:02 -0500 Subject: [PATCH 2/5] delay initalizePayload until clientToken and Payload.js are present, enable onInvalid/Valid on insecure inputs --- src/mappings.js | 2 ++ src/payload-react.js | 26 ++++++++++++++++---------- src/utils.js | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/mappings.js b/src/mappings.js index 96e53fc..2c140e4 100644 --- a/src/mappings.js +++ b/src/mappings.js @@ -39,6 +39,8 @@ export const inputEventsMap = { change: 'onChange', } +export const ignoredEventsForStandardInput = ['onFocus', 'onBlur', 'onChange'] + export const processingFormEventsMap = { success: 'onSuccess', account_created: 'onAccountCreated', diff --git a/src/payload-react.js b/src/payload-react.js index 64bbd15..e662793 100644 --- a/src/payload-react.js +++ b/src/payload-react.js @@ -6,6 +6,7 @@ import { checkoutEventsMap, formEventsMap, formParamsMap, + ignoredEventsForStandardInput, inputEventsMap, inputPropsMap, processingFormAttributeMap, @@ -55,9 +56,12 @@ export class PayloadInput extends React.Component { * Adds event listeners when the component mounts. */ componentDidMount() { - if (!this.isSensitiveField()) return - Object.entries(inputEventsMap).forEach(([key, value]) => { + if ( + !this.isSensitiveField() && + ignoredEventsForStandardInput.includes(value) + ) + return if (value in this.props) this.context.addListener(key, this.inputRef, (...args) => this.props[value](...args) @@ -84,7 +88,10 @@ export class PayloadInput extends React.Component { render() { const attrs = getPropAttrs( this.props, - this.isSensitiveField() ? Object.values(inputEventsMap) : [] + Object.values(inputEventsMap).filter( + (e) => + this.isSensitiveField() || !ignoredEventsForStandardInput.includes(e) + ) ) Object.entries(inputPropsMap).forEach(([key, value]) => { @@ -145,16 +152,12 @@ export class PayloadForm extends React.Component { * @returns {void} */ async componentDidMount() { - if (!this.props.clientToken) { - return - } - if (!this.state.Payload) { if (!window.Payload) { await getPayload() } this.setState((state) => ({ ...state, Payload: window.Payload })) - } else this.initalizePayload() + } else if (this.props.clientToken) this.initalizePayload() } /** @@ -170,10 +173,13 @@ export class PayloadForm extends React.Component { * If this condition is met, it calls the initializePayload method to initialize the Payload.js component. */ componentDidUpdate(prevProps, prevState, snapshot) { - if (!prevState.Payload && this.state.Payload) { + if ( + (!prevState.Payload && this.state.Payload && this.props.clientToken) || + (this.state.Payload && !prevProps.clientToken && this.props.clientToken) + ) { // Payload is set in our state we can initialize it this.initalizePayload() - } else { + } else if (this.state.Payload && this.props.clientToken) { this.updatePayload() } } diff --git a/src/utils.js b/src/utils.js index 3d78269..b65f207 100644 --- a/src/utils.js +++ b/src/utils.js @@ -20,7 +20,7 @@ export function getPayload() { const s = document.createElement('script') - s.setAttribute('src', 'https://payload.com/Payload.js') + s.setAttribute('src', 'http://payload-dev.com:8000/Payload.js') s.addEventListener('load', () => { loadingPromise = null resolve(window.Payload) From 7f42c77dc558be971f34edfdfef39352a5c4e47c Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 28 Feb 2025 16:19:05 -0500 Subject: [PATCH 3/5] fix url in getPayload --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index b65f207..3d78269 100644 --- a/src/utils.js +++ b/src/utils.js @@ -20,7 +20,7 @@ export function getPayload() { const s = document.createElement('script') - s.setAttribute('src', 'http://payload-dev.com:8000/Payload.js') + s.setAttribute('src', 'https://payload.com/Payload.js') s.addEventListener('load', () => { loadingPromise = null resolve(window.Payload) From 490319c3e9cf8021a7322290156ff62ba3c410e7 Mon Sep 17 00:00:00 2001 From: ian Date: Sat, 1 Mar 2025 09:54:04 -0500 Subject: [PATCH 4/5] Added browserlist/node target --- .babelrc | 12 +++++++++++- package-lock.json | 15 ++++++++------- package.json | 6 ++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.babelrc b/.babelrc index 54e0937..2250a39 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,13 @@ { - "presets" : ["@babel/preset-env", "@babel/preset-react"] + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-react" + ] } diff --git a/package-lock.json b/package-lock.json index 6709de5..ee38249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3887,9 +3887,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001521", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", - "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", + "version": "1.0.30001701", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", + "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", "dev": true, "funding": [ { @@ -3904,7 +3904,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -13310,9 +13311,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001521", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", - "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", + "version": "1.0.30001701", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", + "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", "dev": true }, "chalk": { diff --git a/package.json b/package.json index 2ceaae1..b4c47b2 100644 --- a/package.json +++ b/package.json @@ -80,5 +80,11 @@ }, "files": [ "dist" + ], + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" ] } From 2fe81add09063155204404dee5656c21ad573394 Mon Sep 17 00:00:00 2001 From: ian Date: Sat, 1 Mar 2025 09:55:32 -0500 Subject: [PATCH 5/5] remove regenerator-runtime for test setup --- tests/setup-tests.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/setup-tests.js b/tests/setup-tests.js index 6fe98a6..279d5f7 100644 --- a/tests/setup-tests.js +++ b/tests/setup-tests.js @@ -1,4 +1,3 @@ -import 'regenerator-runtime' import { configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-16'