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"