From d8375c048dadc2a96af49c78e74cd4078f4d1a8a Mon Sep 17 00:00:00 2001 From: iman Date: Tue, 26 Nov 2024 16:31:54 +0800 Subject: [PATCH 1/9] wip --- packages/utils/src/flows/foobar-flow.ts | 103 ++++++++++++++++++++++++ packages/vanilla/event.ts | 10 +-- 2 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 packages/utils/src/flows/foobar-flow.ts diff --git a/packages/utils/src/flows/foobar-flow.ts b/packages/utils/src/flows/foobar-flow.ts new file mode 100644 index 0000000..59b417c --- /dev/null +++ b/packages/utils/src/flows/foobar-flow.ts @@ -0,0 +1,103 @@ +import { z } from 'zod'; +import { getPrefill } from '../cde-client'; +import { + addBasicCheckoutCallbackHandlers, + addErrorCatcherForInit, + createOjsFlowLoggers, + InitOjsFlow, + RunOjsFlow, + SimpleOjsFlowResult, +} from './ojs-flow'; +import { findCpmMatchingType } from './common/common-flow-utils'; + +// 👉 Special loggers, edit this to reflect the flow name +const { log__, err__ } = createOjsFlowLoggers('foobar'); + +// 👉 CustomParams are passed when the flow is run. It is optional, and can be undefined +// - Params can be from anywhere, can even be passed from the user +// - Can also be used to differentiate different instances of runFlow +// - E.g. 'apple_pay' or 'google_pay' for running stripe PR flow +export type FoobarFlowCustomParams = { + // Can be of any form +}; + +// 👉 FlowSuccess is returned by initFlow, and is passed to runFlow. +// - If you don't have an init flow, you can remove this type. +export type InitFoobarFlowSuccess = { + // Should conform to InitOjsFlowResult + isAvailable: boolean; +}; + +// 👉 For convenience, you can use zod to define which CPMs are accepted by this flow +export const FoobarCpm = z.object({ + processor_name: z.literal('foobar'), +}); +export type FoobarCpm = z.infer; + +// 👉 initFlow -- this is optional, use this only when you need to initialize something at load time, before runFlow() +// - We use the decorator addErrorCatcherForInit to automatically catch errors. It returns isAvailable: false +/* + * Initializes the Foobar flow (put more details here) + */ +export const initFoobarFlow: InitOjsFlow = addErrorCatcherForInit( + async ({ context }): Promise => { + log__(`Checking if there are any CPMs for Stripe PR...`); + const checkoutPaymentMethod = findCpmMatchingType(context.checkoutPaymentMethods, FoobarCpm); + log__(`checkoutPaymentMethod: ${checkoutPaymentMethod}`); + + // 👉 Examples of logs. You can also use logs as headers/sections of code blocks + log__(`Starting foobar flow...`); + const anyCdeConnection = Array.from(context.cdeConnections.values())[0]; + const prefill = await getPrefill(anyCdeConnection); + const isSetupMode = prefill.mode === 'setup'; + log__(`isSetupMode: ${isSetupMode}`); + err__(`Example of an error log`); + + // 👉 Fill in the rest here + const x = 42; + if (x === 42) { + // 👉 Since we have addErrorCatcherForInit, errors are automatically handled properly + throw new Error('Not implemented yet'); + } + + return { + isAvailable: true, + }; + } +); + +/* + * Runs the main Stripe PaymentRequest flow + */ +export const runFoobarFlow: RunOjsFlow = + addBasicCheckoutCallbackHandlers( + async ({ + context, + checkoutPaymentMethod, + nonCdeFormInputs, + flowCallbacks, + customParams, + initResult, + }): Promise => { + // 👉 There are multiple params passed to runFlow + // - See the definitions in OjsFlowParams for more details + log__('context', context); + log__('checkoutPaymentMethod', checkoutPaymentMethod); + log__('nonCdeFormInputs', nonCdeFormInputs); + log__('flowCallbacks', flowCallbacks); + log__('customParams', customParams); + log__('initResult', initResult); + + // 👉 For the decorator addBasicCheckoutCallbackHandlers, + // we need to return a CheckoutSuccessResponse or SetupCheckoutResponse through SimpleOjsFlowResult + // - CDE functions like performCheckout return these objects for you. See cde-client.ts for more details + return { + mode: 'checkout', + result: { + invoice_urls: [], + subscription_ids: [], + customer_id: '', + }, + }; + } + ); diff --git a/packages/vanilla/event.ts b/packages/vanilla/event.ts index ba35576..8cccc9a 100644 --- a/packages/vanilla/event.ts +++ b/packages/vanilla/event.ts @@ -114,12 +114,12 @@ export class OpenPayFormEventHandler { console.log('handleLoadedEvent is deprecated:', source, elementId, payload); // const status = await start3dsVerification({ url: SIMULATE_3DS_URL, baseUrl: this.config.baseUrl! }); // console.log('🔐 3DS status:', status); - // this.eventTargets[elementId] = source; + this.eventTargets[elementId] = source; // console.log('handleLoadedEvent XXXXXXXXX', payload); - // this.formInstance.onCdeLoaded(payload); - // if (this.config.onLoad) { - // this.config.onLoad(payload.totalAmountAtoms, payload.currency); - // } + this.formInstance.onCdeLoaded(payload); + if (this.config.onLoad) { + this.config.onLoad(payload.totalAmountAtoms, payload.currency); + } } handleLoadErrorEvent(payload: ErrorEventPayload) { From 44d41beeb7ebdcd299688e207e173db8d1b51e21 Mon Sep 17 00:00:00 2001 From: iman Date: Thu, 28 Nov 2024 14:01:25 +0800 Subject: [PATCH 2/9] wip --- apps/vanilla-example/index.html | 5 +- apps/vanilla-example/src/style.css | 4 + packages/react/components/form.tsx | 3 +- packages/utils/src/cde-client.ts | 17 +- packages/utils/src/cde_models.ts | 10 ++ packages/utils/src/flows/all-flows.ts | 5 + .../src/flows/common/common-flow-utils.ts | 19 +- packages/utils/src/flows/foobar-flow.ts | 14 +- packages/utils/src/flows/init-flows.ts | 10 +- packages/utils/src/flows/ojs-flow.ts | 14 ++ .../src/flows/stripe/stripe-link-flow.ts | 165 ++++++++++++++++++ .../utils/src/flows/stripe/stripe-pr-flow.ts | 15 +- packages/utils/src/stripe.ts | 16 +- packages/vanilla/index.ts | 11 +- 14 files changed, 262 insertions(+), 46 deletions(-) create mode 100644 packages/utils/src/flows/stripe/stripe-link-flow.ts diff --git a/apps/vanilla-example/index.html b/apps/vanilla-example/index.html index 89a5684..331723c 100644 --- a/apps/vanilla-example/index.html +++ b/apps/vanilla-example/index.html @@ -86,8 +86,9 @@

OpenPay.js

- - + + +
diff --git a/apps/vanilla-example/src/style.css b/apps/vanilla-example/src/style.css index fc94a22..67619c1 100644 --- a/apps/vanilla-example/src/style.css +++ b/apps/vanilla-example/src/style.css @@ -137,6 +137,10 @@ input { cursor: not-allowed; } +.stripe-link-button { + margin-top: calc(var(--spacing) * 1.4); +} + @keyframes spin { from { transform: rotate(0deg); diff --git a/packages/react/components/form.tsx b/packages/react/components/form.tsx index 92b0255..f02f531 100644 --- a/packages/react/components/form.tsx +++ b/packages/react/components/form.tsx @@ -402,6 +402,7 @@ const ElementsForm: FC = (props) => { elementsSessionId: sessionId, checkoutPaymentMethods, cdeConnections, + customInitParams: {}, }; return context; }; @@ -427,7 +428,7 @@ const ElementsForm: FC = (props) => { if (ojsFlowsInitialization !== null) return; // Initialize only once const context = generateOjsFlowContext(); if (!context) return; - const initialization = initializeOjsFlows(context); + const initialization = initializeOjsFlows(context, ojsFlowCallbacks); setOjsFlowsInitialization(initialization); initialization.stripePR.subscribe((status) => { if (status.status === 'loading') { diff --git a/packages/utils/src/cde-client.ts b/packages/utils/src/cde-client.ts index d52c70f..19484ae 100644 --- a/packages/utils/src/cde-client.ts +++ b/packages/utils/src/cde-client.ts @@ -8,9 +8,7 @@ import { ConfirmPaymentFlowResponse, ElementType, FieldName, - PaymentFlowStartedEventPayload, SetupCheckoutRequest, - SubmitEventPayload, TokenizeCardRequest, TokenizeCardResponse, } from './shared-models'; @@ -24,6 +22,7 @@ import { StartPaymentFlowForCCRequest, StartPaymentFlowForCCResponse, StartPaymentFlowForPRRequest, + StartPaymentFlowRequest, StartPaymentFlowResponse, } from './cde_models'; import { sleep } from './stripe'; @@ -31,9 +30,11 @@ import { sum } from './math'; import { CustomError } from 'ts-custom-error'; /* - * An actual custom Error object, created from a CDEResponseError object - * Note that CDEResponseError is a normal JSON (zod) object returned by CDE endpoints, - * while this class is a real subclass of Error. + * An actual custom Error object, for easier try-catch handling of CDE errors. + * This class is NOT meant to be extended or subclassed. + * + * Note also the difference vs CDEResponseError: + * - CDEResponseError is a normal JSON (zod) object returned by CDE endpoints, while this class is a real subclass of Error. */ export class CdeError extends CustomError { response: CDEResponseError; @@ -93,9 +94,9 @@ export const getPrefill = async (cdeConn: CdeConnection): Promise => { - return await queryCDE(cdeConn, { type: 'start_payment_flow', payload }, PaymentFlowStartedEventPayload); + payload: StartPaymentFlowRequest +): Promise => { + return await queryCDE(cdeConn, { type: 'v2_start_payment_flow', payload }, StartPaymentFlowResponse); }; export const startPaymentFlowForCC = async ( diff --git a/packages/utils/src/cde_models.ts b/packages/utils/src/cde_models.ts index 7b97163..3abeca9 100644 --- a/packages/utils/src/cde_models.ts +++ b/packages/utils/src/cde_models.ts @@ -228,6 +228,16 @@ export type PydanticValidationError = z.infer; export const PydanticValidationErrorResponse = z.array(PydanticValidationError); export type PydanticValidationErrorResponse = z.infer; +// StartPaymentFlowRequest +export const StartPaymentFlowRequest = z.object({ + new_customer_email: z.string().optional(), + new_customer_address: z.record(z.string(), z.any()).optional(), + payment_provider: z.string(), + checkout_payment_method: CheckoutPaymentMethod, + existing_cc_pm_id: z.string().optional(), +}); +export type StartPaymentFlowRequest = z.infer; + // StartPaymentFlowResponse export const StartPaymentFlowResponse = z.object({ required_user_actions: z.array(z.record(z.string(), z.any())), diff --git a/packages/utils/src/flows/all-flows.ts b/packages/utils/src/flows/all-flows.ts index 3213edd..dbfd22a 100644 --- a/packages/utils/src/flows/all-flows.ts +++ b/packages/utils/src/flows/all-flows.ts @@ -1,6 +1,7 @@ import { CheckoutPaymentMethod } from '../shared-models'; import { OjsContext, OjsFlow } from './ojs-flow'; import { runStripeCcFlow } from './stripe/stripe-cc-flow'; +import { initStripeLinkFlow, runStripeLinkFlow } from './stripe/stripe-link-flow'; import { initStripePrFlow, runStripePrFlow } from './stripe/stripe-pr-flow'; export const findCheckoutPaymentMethodStrict = ( @@ -35,6 +36,10 @@ export const OjsFlows = { init: initStripePrFlow, run: runStripePrFlow, }, + stripeLink: { + init: initStripeLinkFlow, + run: runStripeLinkFlow, + }, // 👉 Add more flows here diff --git a/packages/utils/src/flows/common/common-flow-utils.ts b/packages/utils/src/flows/common/common-flow-utils.ts index 18a3279..22849d6 100644 --- a/packages/utils/src/flows/common/common-flow-utils.ts +++ b/packages/utils/src/flows/common/common-flow-utils.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { CheckoutPaymentMethod, ConfirmPaymentFlowResponse } from '../../shared-models'; +import { CheckoutPaymentMethod, ConfirmPaymentFlowResponse, FieldName } from '../../shared-models'; import { createOjsFlowLoggers } from '../ojs-flow'; -const { err__ } = createOjsFlowLoggers('commmon'); +const { log__, err__ } = createOjsFlowLoggers('common'); export type Loadable = | { @@ -33,10 +33,17 @@ export const parseConfirmPaymentFlowResponse = ( export const findCpmMatchingType = (allCPMs: CheckoutPaymentMethod[], zodModel: z.ZodSchema): T => { const cpm = allCPMs.find((cpm) => zodModel.safeParse(cpm).success); if (!cpm) { - err__(`No CPMs found for model ${zodModel}`); - err__(allCPMs); - err__(`zodModel`); - throw new Error(`No CPMs found for model ${zodModel}`); + err__(`No CPMs found for model. All models:`, allCPMs); + throw new Error(`No CPMs found for model`); } return zodModel.parse(cpm); }; + +export const overrideEmptyZipCode = (formInputs: Record): Record => { + const newFormInputs = { ...formInputs }; + if (!newFormInputs[FieldName.ZIP_CODE]) { + log__(`Overriding empty zip code`); + newFormInputs[FieldName.ZIP_CODE] = '00000'; + } + return newFormInputs; +}; diff --git a/packages/utils/src/flows/foobar-flow.ts b/packages/utils/src/flows/foobar-flow.ts index 59b417c..70e3c35 100644 --- a/packages/utils/src/flows/foobar-flow.ts +++ b/packages/utils/src/flows/foobar-flow.ts @@ -13,7 +13,8 @@ import { findCpmMatchingType } from './common/common-flow-utils'; // 👉 Special loggers, edit this to reflect the flow name const { log__, err__ } = createOjsFlowLoggers('foobar'); -// 👉 CustomParams are passed when the flow is run. It is optional, and can be undefined +// 👉 CustomParams are passed when the flow is run. It is optional, and can be undefined. +// - Most of the time, you can remove this type. // - Params can be from anywhere, can even be passed from the user // - Can also be used to differentiate different instances of runFlow // - E.g. 'apple_pay' or 'google_pay' for running stripe PR flow @@ -35,12 +36,17 @@ export const FoobarCpm = z.object({ export type FoobarCpm = z.infer; // 👉 initFlow -- this is optional, use this only when you need to initialize something at load time, before runFlow() -// - We use the decorator addErrorCatcherForInit to automatically catch errors. It returns isAvailable: false +// - We use the decorator addErrorCatcherForInit to automatically catch errors (on error, it returns isAvailable: false) +// - 👉 Note! if you need to pass custom params to initFlow, please pass it through OjsContext.customInitParams /* * Initializes the Foobar flow (put more details here) */ export const initFoobarFlow: InitOjsFlow = addErrorCatcherForInit( async ({ context }): Promise => { + // 👉 If you're planning to have an init flow, please make sure to add it to: + // - 👉 all-flows.ts + // - 👉 init-flows.ts + log__(`Checking if there are any CPMs for Stripe PR...`); const checkoutPaymentMethod = findCpmMatchingType(context.checkoutPaymentMethods, FoobarCpm); log__(`checkoutPaymentMethod: ${checkoutPaymentMethod}`); @@ -79,6 +85,10 @@ export const runFoobarFlow: RunOjsFlow => { + log__(`Running Foobar flow...`); + const anyCdeConnection = Array.from(context.cdeConnections.values())[0]; + log__('anyCdeConnection is convenient if you just need to do a simple CDE query', anyCdeConnection); + // 👉 There are multiple params passed to runFlow // - See the definitions in OjsFlowParams for more details log__('context', context); diff --git a/packages/utils/src/flows/init-flows.ts b/packages/utils/src/flows/init-flows.ts index ebbb0ad..d06062e 100644 --- a/packages/utils/src/flows/init-flows.ts +++ b/packages/utils/src/flows/init-flows.ts @@ -1,6 +1,6 @@ import { OjsFlows } from './all-flows'; import { Loadable } from './common/common-flow-utils'; -import { createOjsFlowLoggers, InitOjsFlowResult, OjsContext } from './ojs-flow'; +import { createOjsFlowLoggers, InitOjsFlowParams, InitOjsFlowResult, OjsContext, OjsFlowCallbacks } from './ojs-flow'; import Observable from 'zen-observable'; const { log__, err__ } = createOjsFlowLoggers('init-flows'); @@ -9,10 +9,14 @@ const { log__, err__ } = createOjsFlowLoggers('init-flows'); * Initializes all OJS flows. * All init flows should be added to this function. */ -export const initializeOjsFlows = (context: OjsContext) => { +export const initializeOjsFlows = (context: OjsContext, flowCallbacks: OjsFlowCallbacks) => { + const initParams: InitOjsFlowParams = { context, flowCallbacks }; return { // Stripe PR - stripePR: runInitFlowAsObservable('stripePR', OjsFlows.stripePR.init({ context })), + stripePR: runInitFlowAsObservable('stripePR', OjsFlows.stripePR.init(initParams)), + + // Stripe Link + stripeLink: runInitFlowAsObservable('stripeLink', OjsFlows.stripeLink.init(initParams)), // 👉 Add initialization flows here }; diff --git a/packages/utils/src/flows/ojs-flow.ts b/packages/utils/src/flows/ojs-flow.ts index 8a6a45b..a734278 100644 --- a/packages/utils/src/flows/ojs-flow.ts +++ b/packages/utils/src/flows/ojs-flow.ts @@ -43,6 +43,11 @@ export type InitOjsFlowParams = { * Ideally this only contains "background" context (OJS-level objects), and not flow-level objects. */ context: OjsContext; + + /** + * Lifecycle callbacks for OJS flows. + */ + flowCallbacks: OjsFlowCallbacks; }; export type InitOjsFlowResult = { @@ -104,6 +109,15 @@ export type OjsContext = { * All the CDE connection objects (one for each CDE iframe). */ cdeConnections: Map; + + /* + * Custom init params + */ + customInitParams: { + stripeLink?: { + buttonHeight?: number; + }; + }; }; /** diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts new file mode 100644 index 0000000..fc89abe --- /dev/null +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -0,0 +1,165 @@ +import { z } from 'zod'; +import { getCheckoutPreviewAmount, getPrefill, performCheckout, startPaymentFlow } from '../../cde-client'; +import { + addBasicCheckoutCallbackHandlers, + addErrorCatcherForInit, + createOjsFlowLoggers, + InitOjsFlow, + InitOjsFlowResult, + RunOjsFlow, + SimpleOjsFlowResult, +} from '../ojs-flow'; +import { findCpmMatchingType, overrideEmptyZipCode } from '../common/common-flow-utils'; +import { PaymentMethod } from '@stripe/stripe-js'; +import { createElementsOptions, createStripeElements, PaymentRequestNextActionMetadata } from '../../stripe'; +import { OjsFlows } from '../all-flows'; +import { createInputsDictFromForm, FieldName } from '../../..'; +import { validateNonCdeFormFieldsForCC } from '../common/cc-flow-utils'; +import { CheckoutRequest, StartPaymentFlowResponse } from '../../cde_models'; + +const OJS_STRIPE_LINK_BTN_ID = 'ojs-stripe-link-btn'; + +const { log__, err__ } = createOjsFlowLoggers('stripe-link'); + +type RunStripeLinkFlowParams = { + stripePM: PaymentMethod; +}; + +export const StripeLinkCpm = z.object({ + provider: z.literal('stripe_link'), + processor_name: z.literal('stripe'), + metadata: z.object({ + stripe_pk: z.string(), + }), +}); +export type StripeLinkCpm = z.infer; + +/* + * Initializes the Stripe link flow (put more details here) + */ +export const initStripeLinkFlow: InitOjsFlow = addErrorCatcherForInit( + async ({ context, flowCallbacks }): Promise => { + log__(`Checking if there are any CPMs for Stripe PR...`); + const stripeLinkCpm = findCpmMatchingType(context.checkoutPaymentMethods, StripeLinkCpm); + + log__(`Starting stripe link flow...`); + const anyCdeConnection = Array.from(context.cdeConnections.values())[0]; + const prefill = await getPrefill(anyCdeConnection); + const isSetupMode = prefill.mode === 'setup'; + + log__(`Creating stripe elements...`); + const initialPreview = await getCheckoutPreviewAmount(anyCdeConnection, prefill.token, isSetupMode, undefined); + const { elements, stripe } = await createStripeElements( + stripeLinkCpm.metadata.stripe_pk, + createElementsOptions(initialPreview) + ); + + log__(`Mounting payment element...`); + const expressCheckoutElement = elements.create('expressCheckout', { + buttonHeight: context.customInitParams.stripeLink?.buttonHeight, + paymentMethods: { + amazonPay: 'never', + applePay: 'never', + googlePay: 'never', + paypal: 'never', + }, + }); + expressCheckoutElement.mount(`#${OJS_STRIPE_LINK_BTN_ID}`); + expressCheckoutElement.on('click', async (event) => { + log__('Stripe Link button clicked. Validating form...'); + const nonCdeFormInputs = createInputsDictFromForm(context.formDiv); + const cleanedFormInputs = overrideEmptyZipCode(nonCdeFormInputs); + // TODO ASAP: maybe use stripe link billing details instead + // If not filled properly, this calls onValidationError callbacks and then throws an error + validateNonCdeFormFieldsForCC(cleanedFormInputs, flowCallbacks.onValidationError); + event.resolve(); + }); + expressCheckoutElement.on('confirm', async (event) => { + log__('Stripe Link window confirmed', event); + const result = await stripe.createPaymentMethod({ + elements, + params: { + billing_details: event.billingDetails, + }, + }); + if (result.error) { + err__('error', result.error); + flowCallbacks.onCheckoutError(result.error.message ?? 'Stripe Link unknown error'); + } else { + log__('paymentMethod', result.paymentMethod); + OjsFlows.stripeLink.run({ + context, + checkoutPaymentMethod: stripeLinkCpm, + nonCdeFormInputs: createInputsDictFromForm(context.formDiv), + flowCallbacks, + customParams: { stripePM: result.paymentMethod }, + initResult: { isAvailable: true }, + }); + } + }); + + return { isAvailable: true }; + } +); + +/* + * Runs the main Stripe link flow + */ +export const runStripeLinkFlow: RunOjsFlow = + addBasicCheckoutCallbackHandlers( + async ({ + context, + checkoutPaymentMethod, + nonCdeFormInputs, + flowCallbacks, + customParams, + initResult, + }): Promise => { + log__(`Running Stripe PR flow...`); + const anyCdeConnection = Array.from(context.cdeConnections.values())[0]; + + console.log('TODO ASAP use these', customParams, initResult); + + log__(`Validating non-CDE form fields`); + const cleanedFormInputs = overrideEmptyZipCode(nonCdeFormInputs); + // TODO ASAP: maybe use stripe link billing details instead + // If not filled properly, this calls onValidationError callbacks and then throws an error + const nonCdeFormFields = validateNonCdeFormFieldsForCC(cleanedFormInputs, flowCallbacks.onValidationError); + + log__(`Starting payment flow...`); + const startPaymentFlowResponse = await startPaymentFlow(anyCdeConnection, { + payment_provider: checkoutPaymentMethod.provider, + checkout_payment_method: checkoutPaymentMethod, + existing_cc_pm_id: 'TODO ASAP', + }); + const nextActionMetadata = parseStripeLinkNextActionMetadata(startPaymentFlowResponse); + log__('nextActionMetadata', nextActionMetadata); + + log__(`Doing checkout...`); + const prefill = await getPrefill(anyCdeConnection); + const checkoutRequest: CheckoutRequest = { + secure_token: prefill.token, + payment_input: { + provider_type: checkoutPaymentMethod.provider, + }, + customer_email: nonCdeFormFields[FieldName.EMAIL], + customer_zip_code: nonCdeFormFields[FieldName.ZIP_CODE], + customer_country: nonCdeFormFields[FieldName.COUNTRY], + promotion_code: nonCdeFormFields[FieldName.PROMOTION_CODE], + line_items: prefill.line_items, + total_amount_atom: prefill.amount_total_atom, + cancel_at_end: false, + checkout_payment_method: checkoutPaymentMethod, + }; + const result = await performCheckout(anyCdeConnection, checkoutRequest); + return { mode: 'checkout', result }; + } + ); + +// TODO ASAP: change the return type +const parseStripeLinkNextActionMetadata = (response: StartPaymentFlowResponse): PaymentRequestNextActionMetadata => { + if (response.required_user_actions.length !== 1) { + throw new Error(`Error occurred.\nDetails: got ${response.required_user_actions.length} required user actions`); + } + return PaymentRequestNextActionMetadata.parse(response.required_user_actions[0]); +}; diff --git a/packages/utils/src/flows/stripe/stripe-pr-flow.ts b/packages/utils/src/flows/stripe/stripe-pr-flow.ts index 2cf122f..617b00c 100644 --- a/packages/utils/src/flows/stripe/stripe-pr-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-pr-flow.ts @@ -24,7 +24,11 @@ import { import { Amount, FieldName } from '../../shared-models'; import { validateNonCdeFormFieldsForCC } from '../common/cc-flow-utils'; import { CheckoutRequest, StartPaymentFlowResponse } from '../../cde_models'; -import { findCpmMatchingType, parseConfirmPaymentFlowResponse } from '../common/common-flow-utils'; +import { + findCpmMatchingType, + overrideEmptyZipCode, + parseConfirmPaymentFlowResponse, +} from '../common/common-flow-utils'; const { log__, err__ } = createOjsFlowLoggers('stripe-pr'); @@ -183,15 +187,6 @@ export const runStripePrFlow: RunOjsFlow): Record => { - const newFormInputs = { ...formInputs }; - if (!newFormInputs[FieldName.ZIP_CODE]) { - log__(`Overriding empty zip code (only for google pay and apple pay)`); - newFormInputs[FieldName.ZIP_CODE] = '00000'; - } - return newFormInputs; -}; - const updatePrWithAmount = (pr: PaymentRequest, amount: Amount, isPending: boolean): void => { pr.update({ total: { diff --git a/packages/utils/src/stripe.ts b/packages/utils/src/stripe.ts index 78900f4..5d3cecc 100644 --- a/packages/utils/src/stripe.ts +++ b/packages/utils/src/stripe.ts @@ -2,7 +2,7 @@ import { PaymentRequest, PaymentRequestPaymentMethodEvent, StripeElements, - StripeElementsOptionsClientSecret, + StripeElementsOptionsMode, Stripe as StripeType, } from '@stripe/stripe-js'; import { Amount, CheckoutPaymentMethod, PaymentFlowStartedEventPayload } from './shared-models'; @@ -54,7 +54,7 @@ const getLoadedStripe = async (publishableKey: string): Promise => { export const createStripeElements = async ( stripePubKey: string, - elementsOptions: StripeElementsOptionsClientSecret + elementsOptions: StripeElementsOptionsMode ): Promise<{ elements: StripeElements; stripe: StripeType }> => { const stripe = await getLoadedStripe(stripePubKey); return { @@ -63,15 +63,17 @@ export const createStripeElements = async ( }; }; -export const createElementsOptions = (amount: Amount): StripeElementsOptionsClientSecret => { - console.log(amount); +// StripeElementsOptionsClientSecret +export const createElementsOptions = (amount: Amount): StripeElementsOptionsMode => { return { // TODO: uncomment these later if we decide to use elements + // TODO ASAP: replace this // clientSecret: 'seti_1QCcXVKKXdhjXGwFhd0btSQD_secret_R4m53UhbceUDHVoxq07r2zoEgqJg7wd', - // mode: 'payment', + mode: 'setup', // amount: amount.amountAtom, - // currency: amount.currency, - // setup_future_usage: 'off_session', + currency: amount.currency, + setup_future_usage: 'off_session', + paymentMethodCreation: 'manual', }; }; diff --git a/packages/vanilla/index.ts b/packages/vanilla/index.ts index 53ac0f8..1739c4e 100644 --- a/packages/vanilla/index.ts +++ b/packages/vanilla/index.ts @@ -126,7 +126,7 @@ export class OpenPayForm { if (this.connectionManager.getAllConnections().size === 0) { return; } - this.ojsFlowsInitialization = initializeOjsFlows(this.createOjsFlowContext()); + this.ojsFlowsInitialization = initializeOjsFlows(this.createOjsFlowContext(), this.createOjsFlowCallbacks()); this.ojsFlowsInitialization.stripePR.subscribe((status) => this.onStripePRStatusChange(status)); }; @@ -232,13 +232,10 @@ export class OpenPayForm { elementsSessionId: this.cdeLoadedPayload.sessionId, checkoutPaymentMethods: this.cdeLoadedPayload.checkoutPaymentMethods, cdeConnections, + customInitParams: {}, }; } - private getNonCdeFormInputs(): Record { - return createInputsDictFromForm(this.getFormDiv()); - } - private createOjsFlowCallbacks() { const noOp = () => {}; return { @@ -258,7 +255,7 @@ export class OpenPayForm { OjsFlows.stripeCC.run({ context, checkoutPaymentMethod: findCheckoutPaymentMethodStrict(context.checkoutPaymentMethods, 'credit_card'), - nonCdeFormInputs: this.getNonCdeFormInputs(), + nonCdeFormInputs: createInputsDictFromForm(context.formDiv), flowCallbacks: this.createOjsFlowCallbacks(), customParams: undefined, // This flow requires no custom params initResult: undefined, // This flow requires no initialization @@ -274,7 +271,7 @@ export class OpenPayForm { return OjsFlows.stripePR.run({ context, checkoutPaymentMethod: findCheckoutPaymentMethodStrict(context.checkoutPaymentMethods, provider), - nonCdeFormInputs: this.getNonCdeFormInputs(), + nonCdeFormInputs: createInputsDictFromForm(context.formDiv), flowCallbacks: this.createOjsFlowCallbacks(), customParams: { provider, overridePaymentRequest: params?.overridePaymentRequest }, initResult, From e8d526c576d37a8c29f558570910929bf6b039c8 Mon Sep 17 00:00:00 2001 From: iman Date: Fri, 29 Nov 2024 21:01:28 +0800 Subject: [PATCH 3/9] fixes --- packages/utils/src/cde_models.ts | 1 + .../src/flows/stripe/stripe-link-flow.ts | 59 +++++++++++-------- packages/vanilla/index.ts | 2 + packages/vanilla/vite-env.d.ts | 1 + 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/utils/src/cde_models.ts b/packages/utils/src/cde_models.ts index 03f4bf1..8a14f6f 100644 --- a/packages/utils/src/cde_models.ts +++ b/packages/utils/src/cde_models.ts @@ -236,6 +236,7 @@ export const StartPaymentFlowRequest = z.object({ payment_provider: z.string(), checkout_payment_method: CheckoutPaymentMethod, existing_cc_pm_id: z.string().optional(), + their_existing_pm_id: z.string().optional(), }); export type StartPaymentFlowRequest = z.infer; diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts index fc89abe..4bdc27b 100644 --- a/packages/utils/src/flows/stripe/stripe-link-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -9,13 +9,13 @@ import { RunOjsFlow, SimpleOjsFlowResult, } from '../ojs-flow'; -import { findCpmMatchingType, overrideEmptyZipCode } from '../common/common-flow-utils'; +import { findCpmMatchingType } from '../common/common-flow-utils'; import { PaymentMethod } from '@stripe/stripe-js'; -import { createElementsOptions, createStripeElements, PaymentRequestNextActionMetadata } from '../../stripe'; +import { createElementsOptions, createStripeElements } from '../../stripe'; import { OjsFlows } from '../all-flows'; import { createInputsDictFromForm, FieldName } from '../../..'; import { validateNonCdeFormFieldsForCC } from '../common/cc-flow-utils'; -import { CheckoutRequest, StartPaymentFlowResponse } from '../../cde_models'; +import { CheckoutRequest } from '../../cde_models'; const OJS_STRIPE_LINK_BTN_ID = 'ojs-stripe-link-btn'; @@ -66,12 +66,7 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche }); expressCheckoutElement.mount(`#${OJS_STRIPE_LINK_BTN_ID}`); expressCheckoutElement.on('click', async (event) => { - log__('Stripe Link button clicked. Validating form...'); - const nonCdeFormInputs = createInputsDictFromForm(context.formDiv); - const cleanedFormInputs = overrideEmptyZipCode(nonCdeFormInputs); - // TODO ASAP: maybe use stripe link billing details instead - // If not filled properly, this calls onValidationError callbacks and then throws an error - validateNonCdeFormFieldsForCC(cleanedFormInputs, flowCallbacks.onValidationError); + log__('Stripe Link button clicked'); event.resolve(); }); expressCheckoutElement.on('confirm', async (event) => { @@ -113,27 +108,21 @@ export const runStripeLinkFlow: RunOjsFlow => { log__(`Running Stripe PR flow...`); const anyCdeConnection = Array.from(context.cdeConnections.values())[0]; - console.log('TODO ASAP use these', customParams, initResult); - - log__(`Validating non-CDE form fields`); - const cleanedFormInputs = overrideEmptyZipCode(nonCdeFormInputs); - // TODO ASAP: maybe use stripe link billing details instead - // If not filled properly, this calls onValidationError callbacks and then throws an error - const nonCdeFormFields = validateNonCdeFormFieldsForCC(cleanedFormInputs, flowCallbacks.onValidationError); + log__(`Merging PM fields with form fields...`); + const mergedInputs = fillEmptyFormInputsWithStripePM(nonCdeFormInputs, customParams.stripePM); + const nonCdeFormFields = validateNonCdeFormFieldsForCC(mergedInputs, flowCallbacks.onValidationError); log__(`Starting payment flow...`); const startPaymentFlowResponse = await startPaymentFlow(anyCdeConnection, { payment_provider: checkoutPaymentMethod.provider, checkout_payment_method: checkoutPaymentMethod, - existing_cc_pm_id: 'TODO ASAP', + their_existing_pm_id: customParams.stripePM.id, }); - const nextActionMetadata = parseStripeLinkNextActionMetadata(startPaymentFlowResponse); - log__('nextActionMetadata', nextActionMetadata); + log__('Start payment flow response', startPaymentFlowResponse); log__(`Doing checkout...`); const prefill = await getPrefill(anyCdeConnection); @@ -156,10 +145,28 @@ export const runStripeLinkFlow: RunOjsFlow { - if (response.required_user_actions.length !== 1) { - throw new Error(`Error occurred.\nDetails: got ${response.required_user_actions.length} required user actions`); - } - return PaymentRequestNextActionMetadata.parse(response.required_user_actions[0]); +const fillEmptyFormInputsWithStripePM = ( + formInputs: Record, + stripePm: PaymentMethod +): Record => { + const inputs = { ...formInputs }; + + // Try splitting full name into first and last + const [payerFirstName, ...payerLastNameParts] = stripePm.billing_details.name?.trim()?.split(/\s+/) ?? []; // Note that first name can also be undefined + const payerLastName = payerLastNameParts.join(' ') || undefined; // Force blank strings to be undefined + + // Note: we use ||, not ?? to ensure that blanks are falsish + inputs[FieldName.FIRST_NAME] = inputs[FieldName.FIRST_NAME] || payerFirstName || '_OP_UNKNOWN'; + inputs[FieldName.LAST_NAME] = inputs[FieldName.LAST_NAME] || payerLastName || '_OP_UNKNOWN'; + inputs[FieldName.EMAIL] = + inputs[FieldName.EMAIL] || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (stripePm as any)['link']?.email || + stripePm.billing_details.email || + 'op_unfilled@getopenpay.com'; + inputs[FieldName.ZIP_CODE] = inputs[FieldName.ZIP_CODE] || stripePm.billing_details.address?.postal_code || '00000'; + inputs[FieldName.COUNTRY] = inputs[FieldName.COUNTRY] || stripePm.billing_details.address?.country || 'US'; + + log__(`Final form inputs:`, inputs); + return inputs; }; diff --git a/packages/vanilla/index.ts b/packages/vanilla/index.ts index 3739df4..dfca045 100644 --- a/packages/vanilla/index.ts +++ b/packages/vanilla/index.ts @@ -52,6 +52,7 @@ export class OpenPayForm { formTarget: string; checkoutFired: boolean; ojsVersion: string; + ojsReleaseVersion: string; private referer: string; private eventHandler: OpenPayFormEventHandler; private formProperties: { height: string }; @@ -79,6 +80,7 @@ export class OpenPayForm { this.connectionManager = new ConnectionManager(); this.eventHandler = new OpenPayFormEventHandler(this); this.ojsVersion = __APP_VERSION__; + this.ojsReleaseVersion = __RELEASE_VERSION__; window.addEventListener('message', this.eventHandler.handleMessage.bind(this.eventHandler)); } diff --git a/packages/vanilla/vite-env.d.ts b/packages/vanilla/vite-env.d.ts index dbb4c62..8bf8e2c 100644 --- a/packages/vanilla/vite-env.d.ts +++ b/packages/vanilla/vite-env.d.ts @@ -1,3 +1,4 @@ /// declare const __APP_VERSION__: string; +declare const __RELEASE_VERSION__: string; From c7f5334628f3f62cc8300431a2c9fc69f1ebe1a3 Mon Sep 17 00:00:00 2001 From: iman Date: Fri, 29 Nov 2024 21:03:45 +0800 Subject: [PATCH 4/9] cleanup --- .../utils/src/flows/stripe/stripe-link-flow.ts | 12 +++++++----- packages/utils/src/stripe.ts | 16 +--------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts index 4bdc27b..f9b57e0 100644 --- a/packages/utils/src/flows/stripe/stripe-link-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -11,7 +11,7 @@ import { } from '../ojs-flow'; import { findCpmMatchingType } from '../common/common-flow-utils'; import { PaymentMethod } from '@stripe/stripe-js'; -import { createElementsOptions, createStripeElements } from '../../stripe'; +import { createStripeElements } from '../../stripe'; import { OjsFlows } from '../all-flows'; import { createInputsDictFromForm, FieldName } from '../../..'; import { validateNonCdeFormFieldsForCC } from '../common/cc-flow-utils'; @@ -49,10 +49,12 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche log__(`Creating stripe elements...`); const initialPreview = await getCheckoutPreviewAmount(anyCdeConnection, prefill.token, isSetupMode, undefined); - const { elements, stripe } = await createStripeElements( - stripeLinkCpm.metadata.stripe_pk, - createElementsOptions(initialPreview) - ); + const { elements, stripe } = await createStripeElements(stripeLinkCpm.metadata.stripe_pk, { + mode: 'setup', + currency: initialPreview.currency, + setup_future_usage: 'off_session', + paymentMethodCreation: 'manual', + }); log__(`Mounting payment element...`); const expressCheckoutElement = elements.create('expressCheckout', { diff --git a/packages/utils/src/stripe.ts b/packages/utils/src/stripe.ts index 61da8a8..9d9fcb9 100644 --- a/packages/utils/src/stripe.ts +++ b/packages/utils/src/stripe.ts @@ -6,7 +6,7 @@ import { Stripe as StripeType, SetupIntentResult, } from '@stripe/stripe-js'; -import { Amount, CheckoutPaymentMethod, PaymentFlowStartedEventPayload } from './shared-models'; +import { CheckoutPaymentMethod, PaymentFlowStartedEventPayload } from './shared-models'; import { z } from 'zod'; export type StripeContext = @@ -64,20 +64,6 @@ export const createStripeElements = async ( }; }; -// StripeElementsOptionsClientSecret -export const createElementsOptions = (amount: Amount): StripeElementsOptionsMode => { - return { - // TODO: uncomment these later if we decide to use elements - // TODO ASAP: replace this - // clientSecret: 'seti_1QCcXVKKXdhjXGwFhd0btSQD_secret_R4m53UhbceUDHVoxq07r2zoEgqJg7wd', - mode: 'setup', - // amount: amount.amountAtom, - currency: amount.currency, - setup_future_usage: 'off_session', - paymentMethodCreation: 'manual', - }; -}; - export const createStripePaymentRequest = async ( stripePubKey: string, totalAmountAtom: number, From d62ff4ddc2c7a33d9e4e57987862dceb4be92077 Mon Sep 17 00:00:00 2001 From: iman Date: Sat, 30 Nov 2024 23:31:06 +0800 Subject: [PATCH 5/9] cleanup --- packages/react/hooks/use-stripe-elements.ts | 73 ------------------- .../src/flows/stripe/stripe-link-flow.ts | 70 +++++++++++++----- 2 files changed, 51 insertions(+), 92 deletions(-) delete mode 100644 packages/react/hooks/use-stripe-elements.ts diff --git a/packages/react/hooks/use-stripe-elements.ts b/packages/react/hooks/use-stripe-elements.ts deleted file mode 100644 index 42d66dd..0000000 --- a/packages/react/hooks/use-stripe-elements.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { CdeConnection, getCheckoutPreviewAmount } from '@getopenpay/utils'; -import { CheckoutPaymentMethod } from '@getopenpay/utils'; -import { DynamicPreview } from '@getopenpay/utils'; -import { useEffect, useState } from 'react'; -import useAsyncEffect from 'use-async-effect'; -import { - createElementsOptions, - createStripeElements, - getGlobalStripeElements, - hasGlobalStripeElements, - parseStripePubKey, - setGlobalStripeElements, -} from '@getopenpay/utils'; -import { getPrefill } from '@getopenpay/utils'; - -export type UseStripeElementsOutput = { - isReady: boolean; -}; - -export const useStripeElements = ( - cdeConn: CdeConnection | null, - secureToken: string | undefined, - availableCPMs: CheckoutPaymentMethod[] | undefined, - formDiv: HTMLDivElement | null, - onUserCompleteUIFlow: (checkoutPaymentMethod: CheckoutPaymentMethod) => void, - dynamicPreview: DynamicPreview -): UseStripeElementsOutput => { - const isLoading = secureToken === undefined || availableCPMs === undefined || !formDiv || !cdeConn; - const previewAmount = dynamicPreview.amount; - const [isSetupMode, setIsSetupMode] = useState(null); - const [isReady, setIsReady] = useState(false); - - // Update elements when preview changes - useEffect(() => { - if (!hasGlobalStripeElements() || !previewAmount || isSetupMode === null) { - return; - } - const elements = getGlobalStripeElements().elements; - elements.update(createElementsOptions(previewAmount)); - }, [previewAmount, isSetupMode]); - - // Stripe link - useAsyncEffect(async () => { - if (isLoading) { - // Do nothing - return; - } - - const stripeLinkCPM = availableCPMs.filter( - (cpm) => cpm.processor_name === 'stripe' && cpm.provider === 'stripe_link' - ); - if (stripeLinkCPM.length === 0) { - throw new Error(`Stripe link is not available as a checkout method`); - } - const stripePubKey = parseStripePubKey(stripeLinkCPM[0].metadata); - const prefill = await getPrefill(cdeConn); - const isSetupMode = prefill.mode === 'setup'; - setIsSetupMode(isSetupMode); - - const initialPreview = await getCheckoutPreviewAmount(cdeConn, secureToken, isSetupMode, undefined); - - const { elements, stripe } = await createStripeElements(stripePubKey, createElementsOptions(initialPreview)); - setGlobalStripeElements(elements, () => onUserCompleteUIFlow(stripeLinkCPM[0]), stripe); - setIsReady(true); - - // const canMakePayment = await pr.canMakePayment(); - // console.log('Can make payment?', canMakePayment); - }, [isLoading]); - - return { - isReady, - }; -}; diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts index f9b57e0..0aac998 100644 --- a/packages/utils/src/flows/stripe/stripe-link-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -1,5 +1,11 @@ import { z } from 'zod'; -import { getCheckoutPreviewAmount, getPrefill, performCheckout, startPaymentFlow } from '../../cde-client'; +import { + confirmPaymentFlow, + getCheckoutPreviewAmount, + getPrefill, + performCheckout, + startPaymentFlow, +} from '../../cde-client'; import { addBasicCheckoutCallbackHandlers, addErrorCatcherForInit, @@ -9,7 +15,7 @@ import { RunOjsFlow, SimpleOjsFlowResult, } from '../ojs-flow'; -import { findCpmMatchingType } from '../common/common-flow-utils'; +import { findCpmMatchingType, parseConfirmPaymentFlowResponse } from '../common/common-flow-utils'; import { PaymentMethod } from '@stripe/stripe-js'; import { createStripeElements } from '../../stripe'; import { OjsFlows } from '../all-flows'; @@ -34,6 +40,16 @@ export const StripeLinkCpm = z.object({ }); export type StripeLinkCpm = z.infer; +// Checkout next_action_metadata in create_their_setup_intent +export const StripeLinkRequiredUserActions = z + .array( + z.object({ + our_existing_pm_id: z.string(), + }) + ) + .length(1); +export type StripeLinkRequiredUserActions = z.infer; + /* * Initializes the Stripe link flow (put more details here) */ @@ -71,6 +87,7 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche log__('Stripe Link button clicked'); event.resolve(); }); + // TODO ASAP: add on click handler so oddsjam can either block or override the click expressCheckoutElement.on('confirm', async (event) => { log__('Stripe Link window confirmed', event); const result = await stripe.createPaymentMethod({ @@ -125,25 +142,40 @@ export const runStripeLinkFlow: RunOjsFlow Date: Sun, 1 Dec 2024 18:31:49 +0800 Subject: [PATCH 6/9] fix --- apps/react-example/src/app/page.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/react-example/src/app/page.tsx b/apps/react-example/src/app/page.tsx index bdbb99d..df03d25 100644 --- a/apps/react-example/src/app/page.tsx +++ b/apps/react-example/src/app/page.tsx @@ -182,20 +182,7 @@ const Form: FC = (props) => { {googlePay.isLoading ? 'Loading' : 'Google Pay'} - {/* - */} - {/* */} + )} From 54a125afb9732c14d06dc4f5d5a30c7171a0c6d3 Mon Sep 17 00:00:00 2001 From: iman Date: Mon, 2 Dec 2024 00:34:23 +0800 Subject: [PATCH 7/9] Add override for link submit button --- apps/react-example/src/app/page.tsx | 5 ++++ apps/vanilla-example/src/main.ts | 5 ++++ packages/react/components/form.tsx | 3 +- packages/utils/src/flows/ojs-flow.ts | 29 ++++++++++++++----- .../src/flows/stripe/stripe-link-flow.ts | 9 ++++-- .../utils/src/flows/stripe/stripe-pr-flow.ts | 1 - packages/utils/src/models.ts | 2 ++ packages/vanilla/index.ts | 4 ++- 8 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/react-example/src/app/page.tsx b/apps/react-example/src/app/page.tsx index df03d25..3ac3213 100644 --- a/apps/react-example/src/app/page.tsx +++ b/apps/react-example/src/app/page.tsx @@ -103,6 +103,11 @@ const Form: FC = (props) => { onSetupPaymentMethodSuccess={onSetupPaymentMethodSuccess} onCheckoutError={onCheckoutError} baseUrl={props.baseUrl ?? process.env.NEXT_PUBLIC_BASE_URL} + customInitParams={{ + stripeLink: { + overrideLinkSubmit: async () => true, + }, + }} > {({ submit, applePay, googlePay, loaded }) => ( diff --git a/apps/vanilla-example/src/main.ts b/apps/vanilla-example/src/main.ts index 14d0cb2..d73582c 100644 --- a/apps/vanilla-example/src/main.ts +++ b/apps/vanilla-example/src/main.ts @@ -125,6 +125,11 @@ function initializeForm(token: string) { googlePayButton.disabled = true; } }, + customInitParams: { + stripeLink: { + overrideLinkSubmit: async () => true, + }, + }, }); previousFormInstance = formInstance; if (separateFrames) { diff --git a/packages/react/components/form.tsx b/packages/react/components/form.tsx index 56285fa..d96d5ab 100644 --- a/packages/react/components/form.tsx +++ b/packages/react/components/form.tsx @@ -49,6 +49,7 @@ const ElementsForm: FC = (props) => { onSetupPaymentMethodSuccess, baseUrl, enableDynamicPreviews, + customInitParams, } = props; const frameBaseUrl: string = baseUrl ?? FRAME_BASE_URL; @@ -402,7 +403,7 @@ const ElementsForm: FC = (props) => { elementsSessionId: sessionId, checkoutPaymentMethods, cdeConnections, - customInitParams: {}, + customInitParams: customInitParams ?? {}, baseUrl: new URL(frameBaseUrl).origin, }; return context; diff --git a/packages/utils/src/flows/ojs-flow.ts b/packages/utils/src/flows/ojs-flow.ts index a85a467..e15f6c4 100644 --- a/packages/utils/src/flows/ojs-flow.ts +++ b/packages/utils/src/flows/ojs-flow.ts @@ -110,14 +110,10 @@ export type OjsContext = { */ cdeConnections: Map; - /* - * Custom init params + /** + * Custom init params for init flows. */ - customInitParams: { - stripeLink?: { - buttonHeight?: number; - }; - }; + customInitParams: CustomInitParams; /** * The base URL of the CDE iframe. @@ -125,6 +121,25 @@ export type OjsContext = { baseUrl: string; }; +export type CustomInitParams = { + stripeLink?: { + /** + * The height of the Stripe Link button. By default, the height of the buttons are 44px. + * You can override this to specify a custom button height in the range of 40px-55px. + * + * See more: https://docs.stripe.com/js/elements_object/create_express_checkout_element#express_checkout_element_create-options-buttonHeight + */ + buttonHeight?: number; + + /** + * If this function returns false, the stripe link submit process is aborted. + * This can be used for additional pre-submit checks (e.g. additional form validation). + * Note that this function must complete within 1 second, or the submission will fail. + */ + overrideLinkSubmit?: () => Promise; + }; +}; + /** * A decorator to automatically handle checkout callbacks for an OJS flow. * The decorated function must return a `SimpleOjsFlowResult` object, not void. diff --git a/packages/utils/src/flows/stripe/stripe-link-flow.ts b/packages/utils/src/flows/stripe/stripe-link-flow.ts index 0aac998..acf17be 100644 --- a/packages/utils/src/flows/stripe/stripe-link-flow.ts +++ b/packages/utils/src/flows/stripe/stripe-link-flow.ts @@ -85,9 +85,15 @@ export const initStripeLinkFlow: InitOjsFlow = addErrorCatche expressCheckoutElement.mount(`#${OJS_STRIPE_LINK_BTN_ID}`); expressCheckoutElement.on('click', async (event) => { log__('Stripe Link button clicked'); + if (context.customInitParams.stripeLink?.overrideLinkSubmit) { + const shouldSubmit = await context.customInitParams.stripeLink.overrideLinkSubmit(); + if (!shouldSubmit) { + log__('Stripe Link submit aborted by overrideLinkSubmit'); + return; + } + } event.resolve(); }); - // TODO ASAP: add on click handler so oddsjam can either block or override the click expressCheckoutElement.on('confirm', async (event) => { log__('Stripe Link window confirmed', event); const result = await stripe.createPaymentMethod({ @@ -155,7 +161,6 @@ export const runStripeLinkFlow: RunOjsFlow void; baseUrl?: string; enableDynamicPreviews?: boolean; + customInitParams?: CustomInitParams; }; diff --git a/packages/vanilla/index.ts b/packages/vanilla/index.ts index dfca045..a32730f 100644 --- a/packages/vanilla/index.ts +++ b/packages/vanilla/index.ts @@ -24,6 +24,7 @@ import { ConnectionManager, createConnection } from './utils/connection'; import { PaymentRequestProvider } from './utils/payment-request'; import { InitStripePrFlowResult, InitStripePrFlowSuccess } from '@getopenpay/utils/src/flows/stripe/stripe-pr-flow'; import { Loadable } from '@getopenpay/utils/src/flows/common/common-flow-utils'; +import { CustomInitParams } from '@getopenpay/utils/src/flows/ojs-flow'; export { FieldName }; export type ElementsFormProps = { @@ -42,6 +43,7 @@ export type ElementsFormProps = { baseUrl?: string; formTarget?: string; onPaymentRequestLoad?: (paymentRequests: Record) => void; + customInitParams?: CustomInitParams; }; export type Config = ElementsFormProps & { _frameUrl?: URL }; @@ -236,7 +238,7 @@ export class OpenPayForm { elementsSessionId: this.cdeLoadedPayload.sessionId, checkoutPaymentMethods: this.cdeLoadedPayload.checkoutPaymentMethods, cdeConnections, - customInitParams: {}, + customInitParams: this.config.customInitParams ?? {}, }; } From f3fa70c6986ea4a62e4efdc58a28ccc6ec01cc43 Mon Sep 17 00:00:00 2001 From: iman Date: Mon, 2 Dec 2024 01:09:17 +0800 Subject: [PATCH 8/9] add more debugging --- packages/utils/src/flows/init-flows.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/utils/src/flows/init-flows.ts b/packages/utils/src/flows/init-flows.ts index d06062e..dc40efc 100644 --- a/packages/utils/src/flows/init-flows.ts +++ b/packages/utils/src/flows/init-flows.ts @@ -5,6 +5,19 @@ import Observable from 'zen-observable'; const { log__, err__ } = createOjsFlowLoggers('init-flows'); +/** + * This object should only be used for debugging purposes. + * If you need to use OJS initialization results for RunFlows, please pass it properly from the corresponding InitFlows. + */ +const getOjsInitResultsDebugObject = () => { + if (!('ojs_init_results' in window)) { + // @ts-expect-error window typing + window['ojs_init_results'] = {}; + } + // @ts-expect-error window typing + return window['ojs_init_results']; +}; + /** * Initializes all OJS flows. * All init flows should be added to this function. @@ -40,9 +53,11 @@ const runInitFlowAsObservable = ( }); }); + // We subscribe right away so that Observables are not lazy and are immediately executed observable.subscribe({ next: (result) => { log__(`${flowName} flow result`, result); + getOjsInitResultsDebugObject()[flowName] = result; }, error: (error) => { err__(`${flowName} flow initialization error:\n${JSON.stringify(error)}`); From 7dff5bf9f34dbd7e112cc9345b3aaa6d1f906ebf Mon Sep 17 00:00:00 2001 From: iman Date: Mon, 2 Dec 2024 15:07:52 +0800 Subject: [PATCH 9/9] fix logs --- packages/utils/src/cde-client.ts | 2 +- packages/utils/src/flows/card/common-cc-flow.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/cde-client.ts b/packages/utils/src/cde-client.ts index ea80b83..72cfa0f 100644 --- a/packages/utils/src/cde-client.ts +++ b/packages/utils/src/cde-client.ts @@ -60,12 +60,12 @@ export const queryCDE = async ( // Leaving these as commented out for easier debugging later console.log('[cde-client] Querying CDE with path and connection:', data.type, cdeConn); const response = await cdeConn.send(data); + console.log('[cde-client] Got response from CDE:', response); if (isCDEResponseError(response)) { throw new CdeError(response); } - console.log('[cde-client] Got response from CDE:', response); if (!checkIfConformsToSchema(response, responseSchema)) { const result = responseSchema.safeParse(response); if (result.success) throw new Error('Invalid state'); diff --git a/packages/utils/src/flows/card/common-cc-flow.ts b/packages/utils/src/flows/card/common-cc-flow.ts index 4875861..0ff9b47 100644 --- a/packages/utils/src/flows/card/common-cc-flow.ts +++ b/packages/utils/src/flows/card/common-cc-flow.ts @@ -62,6 +62,7 @@ export const runCommonCcFlow: RunOjsFlow = addBasicCheckoutCallbackHandlers( // TODO: now we rely on choosing Airwallex vs Stripe by this header // handle multiple processor 3ds + log__('error.response.headers', error.response.headers); const shouldUseNewFlow = error.response.headers?.['op-should-use-new-flow'] === 'true'; const startPfResult = await startPaymentFlowForCC(anyCdeConnection, commonCheckoutParams); log__(`├ op-should-use-new-flow: ${shouldUseNewFlow}`);