From bab513b00ea13c1e91813d63714edf7d08441c81 Mon Sep 17 00:00:00 2001 From: awalker-stripe <51334696+awalker-stripe@users.noreply.github.com> Date: Mon, 14 Nov 2022 16:23:31 -0800 Subject: [PATCH] Add component for payButton element (#344) * bump stripe-js version * Add PBE component * Custom ready payload * Copy/paste + declare PayButtonElement --- package.json | 4 +- .../createElementComponent.test.tsx | 114 ++++++++++++++++++ src/components/createElementComponent.tsx | 43 +++++++ src/index.ts | 12 ++ src/types/index.ts | 70 ++++++++++- yarn.lock | 8 +- 6 files changed, 244 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 54bcda5..841d261 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@babel/preset-env": "^7.7.1", "@babel/preset-react": "^7.7.0", "@storybook/react": "^6.5.0-beta.8", - "@stripe/stripe-js": "^1.42.0", + "@stripe/stripe-js": "^1.44.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/react-hooks": "^8.0.0", @@ -106,7 +106,7 @@ "@types/react": "18.0.5" }, "peerDependencies": { - "@stripe/stripe-js": "^1.42.1", + "@stripe/stripe-js": "^1.44.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } diff --git a/src/components/createElementComponent.test.tsx b/src/components/createElementComponent.test.tsx index acf472e..a712e08 100644 --- a/src/components/createElementComponent.test.tsx +++ b/src/components/createElementComponent.test.tsx @@ -9,6 +9,7 @@ import { PaymentElementComponent, PaymentRequestButtonElementComponent, CartElementComponent, + PayButtonElementComponent, } from '../types'; const {Elements} = ElementsModule; @@ -29,6 +30,10 @@ describe('createElementComponent', () => { let simulateNetworksChange: any; let simulateCheckout: any; let simulateLineItemClick: any; + let simulateConfirm: any; + let simulateCancel: any; + let simulateShippingAddressChange: any; + let simulateShippingRateChange: any; beforeEach(() => { mockStripe = mocks.mockStripe(); @@ -72,6 +77,18 @@ describe('createElementComponent', () => { case 'lineitemclick': simulateLineItemClick = fn; break; + case 'confirm': + simulateConfirm = fn; + break; + case 'cancel': + simulateCancel = fn; + break; + case 'shippingaddresschange': + simulateShippingAddressChange = fn; + break; + case 'shippingratechange': + simulateShippingRateChange = fn; + break; default: throw new Error('TestSetupError: Unexpected event registration.'); } @@ -159,6 +176,11 @@ describe('createElementComponent', () => { false ); + const PayButtonElement: PayButtonElementComponent = createElementComponent( + 'payButton', + false + ); + it('Can remove and add CardElement at the same time', () => { let cardMounted = false; mockElement.mount.mockImplementation(() => { @@ -534,6 +556,98 @@ describe('createElementComponent', () => { expect(mockHandler).not.toHaveBeenCalled(); }); + it('propagates the Element`s confirm event to the current onConfirm prop', () => { + const mockHandler = jest.fn(); + const mockHandler2 = jest.fn(); + const {rerender} = render( + + + + ); + rerender( + + + + ); + + const confirmEventMock = Symbol('confirm'); + simulateConfirm(confirmEventMock); + expect(mockHandler2).toHaveBeenCalledWith(confirmEventMock); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('propagates the Element`s cancel event to the current onCancel prop', () => { + const mockHandler = jest.fn(); + const mockHandler2 = jest.fn(); + const {rerender} = render( + + {}} onCancel={mockHandler} /> + + ); + rerender( + + {}} onCancel={mockHandler2} /> + + ); + + const cancelEventMock = Symbol('cancel'); + simulateCancel(cancelEventMock); + expect(mockHandler2).toHaveBeenCalledWith(cancelEventMock); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('propagates the Element`s shippingaddresschange event to the current onShippingAddressChange prop', () => { + const mockHandler = jest.fn(); + const mockHandler2 = jest.fn(); + const {rerender} = render( + + {}} + onShippingAddressChange={mockHandler} + /> + + ); + rerender( + + {}} + onShippingAddressChange={mockHandler2} + /> + + ); + + const shippingAddressChangeEventMock = Symbol('shippingaddresschange'); + simulateShippingAddressChange(shippingAddressChangeEventMock); + expect(mockHandler2).toHaveBeenCalledWith(shippingAddressChangeEventMock); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('propagates the Element`s shippingratechange event to the current onShippingRateChange prop', () => { + const mockHandler = jest.fn(); + const mockHandler2 = jest.fn(); + const {rerender} = render( + + {}} + onShippingRateChange={mockHandler} + /> + + ); + rerender( + + {}} + onShippingRateChange={mockHandler2} + /> + + ); + + const shippingRateChangeEventMock = Symbol('shippingratechange'); + simulateShippingRateChange(shippingRateChangeEventMock); + expect(mockHandler2).toHaveBeenCalledWith(shippingRateChangeEventMock); + expect(mockHandler).not.toHaveBeenCalled(); + }); + it('updates the Element when options change', () => { const {rerender} = render( diff --git a/src/components/createElementComponent.tsx b/src/components/createElementComponent.tsx index 27e3b3a..abd4e8e 100644 --- a/src/components/createElementComponent.tsx +++ b/src/components/createElementComponent.tsx @@ -34,6 +34,10 @@ interface PrivateElementProps { onNetworksChange?: UnknownCallback; onCheckout?: UnknownCallback; onLineItemClick?: UnknownCallback; + onConfirm?: UnknownCallback; + onCancel?: UnknownCallback; + onShippingAddressChange?: UnknownCallback; + onShippingRateChange?: UnknownCallback; options?: UnknownOptions; } @@ -62,6 +66,10 @@ const createElementComponent = ( onNetworksChange = noop, onCheckout = noop, onLineItemClick = noop, + onConfirm = noop, + onCancel = noop, + onShippingAddressChange = noop, + onShippingRateChange = noop, }) => { const {elements} = useElementsContextWithUseCase(`mounts <${displayName}>`); const elementRef = React.useRef(null); @@ -82,6 +90,12 @@ const createElementComponent = ( const callOnNetworksChange = useCallbackReference(onNetworksChange); const callOnCheckout = useCallbackReference(onCheckout); const callOnLineItemClick = useCallbackReference(onLineItemClick); + const callOnConfirm = useCallbackReference(onConfirm); + const callOnCancel = useCallbackReference(onCancel); + const callOnShippingAddressChange = useCallbackReference( + onShippingAddressChange + ); + const callOnShippingRateChange = useCallbackReference(onShippingRateChange); React.useLayoutEffect(() => { if (elementRef.current == null && elements && domNode.current != null) { @@ -104,6 +118,8 @@ const createElementComponent = ( } // the cart ready event returns a CartStatePayload instead of the CartElement callOnReady(event); + } else if (type === 'payButton') { + callOnReady(event); } else { callOnReady(element); } @@ -174,6 +190,29 @@ const createElementComponent = ( // just as they could listen for the `lineitemclick` event on any Element, // but only certain Elements will trigger the event. (element as any).on('lineitemclick', callOnLineItemClick); + + // Users can pass an onConfirm prop on any Element component + // just as they could listen for the `confirm` event on any Element, + // but only certain Elements will trigger the event. + (element as any).on('confirm', callOnConfirm); + + // Users can pass an onCancel prop on any Element component + // just as they could listen for the `cancel` event on any Element, + // but only certain Elements will trigger the event. + (element as any).on('cancel', callOnCancel); + + // Users can pass an onShippingAddressChange prop on any Element component + // just as they could listen for the `shippingaddresschange` event on any Element, + // but only certain Elements will trigger the event. + (element as any).on( + 'shippingaddresschange', + callOnShippingAddressChange + ); + + // Users can pass an onShippingRateChange prop on any Element component + // just as they could listen for the `shippingratechange` event on any Element, + // but only certain Elements will trigger the event. + (element as any).on('shippingratechange', callOnShippingRateChange); } }); @@ -229,6 +268,10 @@ const createElementComponent = ( onNetworksChange: PropTypes.func, onCheckout: PropTypes.func, onLineItemClick: PropTypes.func, + onConfirm: PropTypes.func, + onCancel: PropTypes.func, + onShippingAddressChange: PropTypes.func, + onShippingRateChange: PropTypes.func, options: PropTypes.object as any, }; diff --git a/src/index.ts b/src/index.ts index c43daa2..b1ea41c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ import { AffirmMessageElementComponent, AfterpayClearpayMessageElementComponent, PaymentMethodMessagingElementComponent, + PayButtonElementComponent, } from './types'; export * from './types'; @@ -122,6 +123,17 @@ export const PaymentElement: PaymentElementComponent = createElementComponent( isServer ); +/** + * Requires beta access: + * Contact [Stripe support](https://support.stripe.com/) for more information. + * + * @docs https://stripe.com/docs/stripe-js/react#element-components + */ +export const PayButtonElement: PayButtonElementComponent = createElementComponent( + 'payButton', + isServer +); + /** * @docs https://stripe.com/docs/stripe-js/react#element-components */ diff --git a/src/types/index.ts b/src/types/index.ts index f58075d..1c81901 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -431,6 +431,62 @@ export interface PaymentElementProps extends ElementProps { export type PaymentElementComponent = FunctionComponent; +export interface PayButtonElementProps extends ElementProps { + /** + * An object containing Element configuration options. + */ + options?: stripeJs.StripePayButtonElementOptions; + + /** + * Triggered when the Element is fully rendered and can accept imperative `element.focus()` calls. + * The list of payment methods that could possibly show in the element, or undefined if no payment methods can show. + */ + onReady?: (event: stripeJs.StripePayButtonElementReadyEvent) => any; + + /** + * Triggered when the escape key is pressed within the Element. + */ + onEscape?: () => any; + + /** + * Triggered when the Element fails to load. + */ + onLoadError?: (event: {elementType: 'payButton'; error: StripeError}) => any; + + /** + * Triggered when a button on the Element is clicked. + */ + onClick?: (event: stripeJs.StripePayButtonElementClickEvent) => any; + + /** + * Triggered when a buyer authorizes a payment within a supported payment method. + */ + onConfirm: (event: stripeJs.StripePayButtonElementConfirmEvent) => any; + + /** + * Triggered when a payment interface is dismissed (e.g., a buyer closes the payment interface) + */ + onCancel?: (event: {elementType: 'payButton'}) => any; + + /** + * Triggered when a buyer selects a different shipping address. + */ + onShippingAddressChange?: ( + event: stripeJs.StripePayButtonElementShippingAddressChangeEvent + ) => any; + + /** + * Triggered when a buyer selects a different shipping rate. + */ + onShippingRateChange?: ( + event: stripeJs.StripePayButtonElementShippingRateChangeEvent + ) => any; +} + +export type PayButtonElementComponent = FunctionComponent< + PayButtonElementProps +>; + export interface PaymentRequestButtonElementProps extends ElementProps { /** * An object containing [Element configuration options](https://stripe.com/docs/js/elements_object/create_element?type=paymentRequestButton). @@ -707,17 +763,29 @@ declare module '@stripe/stripe-js' { ): stripeJs.StripeEpsBankElement | null; /** - * Returns the underlying [element instance](https://stripe.com/docs/js/elements_object/create_element?type=card) for the `LinkAuthenticationElement` component in the current [Elements](https://stripe.com/docs/stripe-js/react#elements-provider) provider tree. + * Returns the underlying [element instance](https://stripe.com/docs/js/elements_object/create_link_authentication_element) for the `LinkAuthenticationElement` component in the current [Elements](https://stripe.com/docs/stripe-js/react#elements-provider) provider tree. * Returns `null` if no `LinkAuthenticationElement` is rendered in the current `Elements` provider tree. */ getElement( component: LinkAuthenticationElementComponent ): stripeJs.StripeLinkAuthenticationElement | null; + /** + * Returns the underlying [element instance](https://stripe.com/docs/js/elements_object/create_payment_element) for the `PaymentElement` component in the current [Elements](https://stripe.com/docs/stripe-js/react#elements-provider) provider tree. + * Returns `null` if no `PaymentElement` is rendered in the current `Elements` provider tree. + */ getElement( component: PaymentElementComponent ): stripeJs.StripeElement | null; + /** + * Returns the underlying [element instance](https://stripe.com/docs/js/elements_object/create_pay_button_element) for the `PayButtonElement` component in the current [Elements](https://stripe.com/docs/stripe-js/react#elements-provider) provider tree. + * Returns `null` if no `PayButtonElement` is rendered in the current `Elements` provider tree. + */ + getElement( + component: PayButtonElementComponent + ): stripeJs.StripeElement | null; + /** * Returns the underlying [element instance](https://stripe.com/docs/js/elements_object/create_element?type=card) for the `PaymentRequestButtonElement` component in the current [Elements](https://stripe.com/docs/stripe-js/react#elements-provider) provider tree. * Returns `null` if no `PaymentRequestButtonElement` is rendered in the current `Elements` provider tree. diff --git a/yarn.lock b/yarn.lock index 08525d7..faac309 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2130,10 +2130,10 @@ regenerator-runtime "^0.13.7" resolve-from "^5.0.0" -"@stripe/stripe-js@^1.42.0": - version "1.42.0" - resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.42.0.tgz#6074d0ac184bd70c9e5b5bc00e126719277e0128" - integrity sha512-ZaQpZo5PRv/mN6157OywMW4fc7FIS+eI/tS12I1gU9MdvnL8fzFktuAU/g9FyUOL22E4t9hF1tsfLQAZcQDokQ== +"@stripe/stripe-js@^1.44.1": + version "1.44.1" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.44.1.tgz#376fdbed2b394c84deaa2041b8029b97e7eab3a7" + integrity sha512-DKj3U6tS+sCNsSXsoZbOl5gDrAVD3cAZ9QCiVSykLC3iJo085kkmw/3BAACRH54Bq2bN34yySuH6G1SLh2xHXA== "@testing-library/dom@^8.5.0": version "8.13.0"