diff --git a/apps/react-example/src/app/page.tsx b/apps/react-example/src/app/page.tsx index 3ac3213..4d304b5 100644 --- a/apps/react-example/src/app/page.tsx +++ b/apps/react-example/src/app/page.tsx @@ -28,6 +28,7 @@ const Form: FC = (props) => { } | null>(null); const [validationErrors, setValidationErrors] = useState>({}); + const [stripeLinkShown, setStripeLinkShown] = useState(true); const prParams = { overridePaymentRequest: { @@ -106,10 +107,11 @@ const Form: FC = (props) => { customInitParams={{ stripeLink: { overrideLinkSubmit: async () => true, + doNotMountOnInit: false, }, }} > - {({ submit, applePay, googlePay, loaded }) => ( + {({ submit, applePay, googlePay, loaded, stripeLink }) => ( {loading && (
@@ -186,8 +188,35 @@ const Form: FC = (props) => { > {googlePay.isLoading ? 'Loading' : 'Google Pay'} - - + { + stripeLinkShown ? + + : + <> + } +
+ { + stripeLink && + + } +
)} diff --git a/package-lock.json b/package-lock.json index be7928f..501a271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7604,7 +7604,7 @@ }, "packages/react": { "name": "@getopenpay/openpay-js-react", - "version": "0.1.9", + "version": "0.1.10", "license": "ISC", "dependencies": { "chalk": "^5.3.0", @@ -7686,7 +7686,7 @@ }, "packages/vanilla": { "name": "@getopenpay/openpay-js", - "version": "0.1.9", + "version": "0.1.10", "license": "ISC", "dependencies": { "chalk": "^5.3.0", diff --git a/packages/react/components/form.tsx b/packages/react/components/form.tsx index f8a92af..2ee80ba 100644 --- a/packages/react/components/form.tsx +++ b/packages/react/components/form.tsx @@ -16,6 +16,7 @@ import { PaymentRequestStatus, PR_ERROR, PR_LOADING, + StripeLinkController, } from '@getopenpay/utils'; import { ElementsFormChildrenProps, ElementsFormProps } from '@getopenpay/utils'; import { CheckoutPaymentMethod, EventType, SubmitEventPayload } from '@getopenpay/utils'; @@ -80,6 +81,7 @@ const ElementsForm: FC = (props) => { apple_pay: PR_LOADING, google_pay: PR_LOADING, }); + const [stripeLinkCtrl, setStripeLinkCtrl] = useState(null); useEffect(() => { const ojs_version = { version: __APP_VERSION__, release_version: __RELEASE_VERSION__ }; @@ -462,6 +464,11 @@ const ElementsForm: FC = (props) => { startFlow: async (userParams) => (canGooglePay ? submitPR('google_pay', initResult, userParams) : undefined), }); } + initialization.stripeLink.subscribe((init) => { + if (init.status === 'loaded' && init.result.isAvailable) { + setStripeLinkCtrl(init.result.controller); + } + }); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [formRef.current, sessionId, checkoutPaymentMethods, numCdeConns === 0]); @@ -530,49 +537,6 @@ const ElementsForm: FC = (props) => { [iframes] ); - // const onUserCompletePaymentRequestUI = async ( - // stripePm: PaymentRequestPaymentMethodEvent | null, - // checkoutPaymentMethod: CheckoutPaymentMethod - // ): Promise => { - // if (!formRef.current || !onValidationError || !sessionId || !checkoutPaymentMethods) return; - // // Try all iframe targets, note that this loop will break as soon as one succeeds - // for (const [elementId, target] of Object.entries(eventTargets)) { - // if (!target) continue; - // const paymentFlowMetadata = stripePm - // ? { - // stripePmId: stripePm.paymentMethod.id, - // checkoutPaymentMethod, - // } - // : { - // checkoutPaymentMethod, - // }; - // const startPaymentFlowEvent = constructSubmitEventPayload( - // EventType.enum.START_PAYMENT_FLOW, - // sessionId, - // formRef.current, - // onValidationError, - // checkoutPaymentMethod, - // false, - // paymentFlowMetadata - // ); - // if (!startPaymentFlowEvent) continue; - // if (stripePm) { - // setStripePm(stripePm); - // } - // setCheckoutFired(true); - // setExtraData(startPaymentFlowEvent); - // emitEvent(target, formId, elementId, startPaymentFlowEvent, frameBaseUrl); - // // If first one succeeds, break - // break; - // } - // }; - - // const onPaymentRequestError = (errMsg: string): void => { - // console.error('[form] Error from payment request:', errMsg); - // setCheckoutFired(false); - // if (onCheckoutError) onCheckoutError(errMsg); - // }; - const value: ElementsContextValue = { formId, formHeight, @@ -582,41 +546,13 @@ const ElementsForm: FC = (props) => { baseUrl: frameBaseUrl, }; - // const paymentRequests = usePaymentRequests( - // anyCdeConn, - // checkoutSecureToken, - // checkoutPaymentMethods, - // formRef.current, - // onUserCompletePaymentRequestUI, - // onValidationError, - // onPaymentRequestError, - // dynamicPreview - // ); - - // TODO: uncomment if we need stripe elements later. - // Better if we can make stripe link work in prod without elements. - // const stripeElements = useStripeElements( - // cdeConn, - // checkoutSecureToken, - // checkoutPaymentMethods, - // formRef.current, - // (cpm: CheckoutPaymentMethod) => onUserCompletePaymentRequestUI(null, cpm), - // dynamicPreview - // ); - const childrenProps: ElementsFormChildrenProps = { submit: submitCard, applePay: paymentRequests.apple_pay, googlePay: paymentRequests.google_pay, - // stripeLink: { - // TODO: uncomment if we need stripe elements later. - // Better if we can make stripe link work in prod without elements. - // button: stripeElements.isReady ? StripeLinkButton : NoOpElement, - // authElement: stripeElements.isReady ? StripeLinkAuthElement : NoOpElement, - // pr: paymentRequests.stripe_link, - // }, loaded, preview: dynamicPreview, + stripeLink: stripeLinkCtrl, }; return ( diff --git a/packages/react/package.json b/packages/react/package.json index 1ed9011..af3b2ff 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@getopenpay/openpay-js-react", - "version": "0.1.10", + "version": "0.1.11", "description": "Accept payments through OpenPay, right on your site", "author": "OpenPay (https://getopenpay.com)", "private": false, diff --git a/packages/utils/src/flows/init-flows.ts b/packages/utils/src/flows/init-flows.ts index e1645fc..51e8e14 100644 --- a/packages/utils/src/flows/init-flows.ts +++ b/packages/utils/src/flows/init-flows.ts @@ -2,6 +2,7 @@ import { OjsFlows } from './all-flows'; import { Loadable } from './common/common-flow-utils'; import { createOjsFlowLoggers, InitOjsFlowParams, InitOjsFlowResult, OjsContext, OjsFlowCallbacks } from './ojs-flow'; import Observable from 'zen-observable'; +import { StripeLinkController } from './stripe/stripe-link-flow'; const { log__, err__ } = createOjsFlowLoggers('init-flows'); @@ -71,3 +72,4 @@ const runInitFlowAsObservable = ( }; export type OjsFlowsInitialization = ReturnType; +export type { StripeLinkController }; diff --git a/packages/utils/src/flows/ojs-flow.ts b/packages/utils/src/flows/ojs-flow.ts index e15f6c4..d347d2c 100644 --- a/packages/utils/src/flows/ojs-flow.ts +++ b/packages/utils/src/flows/ojs-flow.ts @@ -137,6 +137,12 @@ export type CustomInitParams = { * Note that this function must complete within 1 second, or the submission will fail. */ overrideLinkSubmit?: () => Promise; + + /** + * By default, the stripe link button is mounted on OJS initialization. + * If this value is true, the stripe link is not mounted on init, and should instead be manually mounted. + */ + doNotMountOnInit?: boolean; }; }; diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts index acf17be..23b5f92 100644 --- a/packages/utils/src/flows/stripe/stripe-link-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -31,6 +31,21 @@ type RunStripeLinkFlowParams = { stripePM: PaymentMethod; }; +export type StripeLinkController = { + mountButton: () => void; + dismountButton: () => void; + waitForButtonToMount: () => Promise; +}; + +export type InitStripeLinkFlowResult = + | { + isAvailable: true; + controller: StripeLinkController; + } + | { + isAvailable: false; + }; + export const StripeLinkCpm = z.object({ provider: z.literal('stripe_link'), processor_name: z.literal('stripe'), @@ -53,8 +68,10 @@ export type StripeLinkRequiredUserActions = z.infer = addErrorCatcherForInit( - async ({ context, flowCallbacks }): Promise => { +export const initStripeLinkFlow: InitOjsFlow = addErrorCatcherForInit( + async ({ context, flowCallbacks }): Promise => { + const initParams = context.customInitParams.stripeLink; + log__(`Checking if there are any CPMs for Stripe PR...`); const stripeLinkCpm = findCpmMatchingType(context.checkoutPaymentMethods, StripeLinkCpm); @@ -72,9 +89,9 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche paymentMethodCreation: 'manual', }); - log__(`Mounting payment element...`); + log__(`Creating express checkout...`); const expressCheckoutElement = elements.create('expressCheckout', { - buttonHeight: context.customInitParams.stripeLink?.buttonHeight, + buttonHeight: initParams?.buttonHeight, paymentMethods: { amazonPay: 'never', applePay: 'never', @@ -82,11 +99,26 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche paypal: 'never', }, }); - expressCheckoutElement.mount(`#${OJS_STRIPE_LINK_BTN_ID}`); + + // Mounting + const mountButton = () => { + expressCheckoutElement.mount(`#${OJS_STRIPE_LINK_BTN_ID}`); + }; + const dismountButton = () => { + expressCheckoutElement.unmount(); + }; + if (initParams?.doNotMountOnInit) { + log__(`NOT mounting stripe link button (doNotMountOnInit is true)`); + } else { + log__(`Mounting stripe link button...`); + mountButton(); + } + + // Add listeners expressCheckoutElement.on('click', async (event) => { log__('Stripe Link button clicked'); - if (context.customInitParams.stripeLink?.overrideLinkSubmit) { - const shouldSubmit = await context.customInitParams.stripeLink.overrideLinkSubmit(); + if (initParams?.overrideLinkSubmit) { + const shouldSubmit = await initParams.overrideLinkSubmit(); if (!shouldSubmit) { log__('Stripe Link submit aborted by overrideLinkSubmit'); return; @@ -118,7 +150,14 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche } }); - return { isAvailable: true }; + return { + isAvailable: true, + controller: { + mountButton, + dismountButton, + waitForButtonToMount: async () => await getElementByIdAsync(OJS_STRIPE_LINK_BTN_ID), + }, + }; } ); @@ -209,3 +248,16 @@ const fillEmptyFormInputsWithStripePM = ( log__(`Final form inputs:`, inputs); return inputs; }; + +const getElementByIdAsync = (id: string) => + new Promise((resolve) => { + const getElement = () => { + const element = document.getElementById(id); + if (element) { + resolve(element); + } else { + requestAnimationFrame(getElement); + } + }; + getElement(); + }); diff --git a/packages/utils/src/models.ts b/packages/utils/src/models.ts index 8a26a96..4c72eba 100644 --- a/packages/utils/src/models.ts +++ b/packages/utils/src/models.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { AllFieldNames, Amount, type ElementsStyle, PaymentRequestStatus } from './shared-models'; import { CustomInitParams } from './flows/ojs-flow'; +import { StripeLinkController } from '../dist'; export type DynamicPreview = { amount: Amount | null; @@ -16,11 +17,7 @@ export type ElementsFormChildrenProps = { submit: () => void; applePay: PaymentRequestStatus; googlePay: PaymentRequestStatus; - // stripeLink: { - // button: FC; - // authElement: FC; - // pr: PaymentRequestStatus; - // }; + stripeLink: StripeLinkController | null; loaded: boolean; preview: DynamicPreview; }; diff --git a/packages/vanilla/package.json b/packages/vanilla/package.json index 4630dd9..a1c8b76 100644 --- a/packages/vanilla/package.json +++ b/packages/vanilla/package.json @@ -1,6 +1,6 @@ { "name": "@getopenpay/openpay-js", - "version": "0.1.10", + "version": "0.1.11", "description": "Accept payments through OpenPay, right on your site", "author": "OpenPay (https://getopenpay.com)", "license": "ISC",