Skip to content

Commit

Permalink
Add component for payButton element (#344)
Browse files Browse the repository at this point in the history
* bump stripe-js version

* Add PBE component

* Custom ready payload

* Copy/paste + declare PayButtonElement
  • Loading branch information
awalker-stripe authored Nov 15, 2022
1 parent c11af14 commit bab513b
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 7 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
Expand Down
114 changes: 114 additions & 0 deletions src/components/createElementComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PaymentElementComponent,
PaymentRequestButtonElementComponent,
CartElementComponent,
PayButtonElementComponent,
} from '../types';

const {Elements} = ElementsModule;
Expand All @@ -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();
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(
<Elements stripe={mockStripe}>
<PayButtonElement onConfirm={mockHandler} />
</Elements>
);
rerender(
<Elements stripe={mockStripe}>
<PayButtonElement onConfirm={mockHandler2} />
</Elements>
);

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(
<Elements stripe={mockStripe}>
<PayButtonElement onConfirm={() => {}} onCancel={mockHandler} />
</Elements>
);
rerender(
<Elements stripe={mockStripe}>
<PayButtonElement onConfirm={() => {}} onCancel={mockHandler2} />
</Elements>
);

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(
<Elements stripe={mockStripe}>
<PayButtonElement
onConfirm={() => {}}
onShippingAddressChange={mockHandler}
/>
</Elements>
);
rerender(
<Elements stripe={mockStripe}>
<PayButtonElement
onConfirm={() => {}}
onShippingAddressChange={mockHandler2}
/>
</Elements>
);

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(
<Elements stripe={mockStripe}>
<PayButtonElement
onConfirm={() => {}}
onShippingRateChange={mockHandler}
/>
</Elements>
);
rerender(
<Elements stripe={mockStripe}>
<PayButtonElement
onConfirm={() => {}}
onShippingRateChange={mockHandler2}
/>
</Elements>
);

const shippingRateChangeEventMock = Symbol('shippingratechange');
simulateShippingRateChange(shippingRateChangeEventMock);
expect(mockHandler2).toHaveBeenCalledWith(shippingRateChangeEventMock);
expect(mockHandler).not.toHaveBeenCalled();
});

it('updates the Element when options change', () => {
const {rerender} = render(
<Elements stripe={mockStripe}>
Expand Down
43 changes: 43 additions & 0 deletions src/components/createElementComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ interface PrivateElementProps {
onNetworksChange?: UnknownCallback;
onCheckout?: UnknownCallback;
onLineItemClick?: UnknownCallback;
onConfirm?: UnknownCallback;
onCancel?: UnknownCallback;
onShippingAddressChange?: UnknownCallback;
onShippingRateChange?: UnknownCallback;
options?: UnknownOptions;
}

Expand Down Expand Up @@ -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<stripeJs.StripeElement | null>(null);
Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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,
};

Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
AffirmMessageElementComponent,
AfterpayClearpayMessageElementComponent,
PaymentMethodMessagingElementComponent,
PayButtonElementComponent,
} from './types';

export * from './types';
Expand Down Expand Up @@ -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
*/
Expand Down
70 changes: 69 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,62 @@ export interface PaymentElementProps extends ElementProps {

export type PaymentElementComponent = FunctionComponent<PaymentElementProps>;

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).
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit bab513b

Please sign in to comment.