Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Common CC flow with different 3DS flows [airwallex,stripe] #70

Merged
merged 8 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/react/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ const ElementsForm: FC<ElementsFormProps> = (props) => {
const submitCard = () => {
const context = generateOjsFlowContext();
if (!formRef.current || !sessionId || !anyCdeConn || !checkoutPaymentMethods || !context) return;
OjsFlows.stripeCC.run({
OjsFlows.commonCC.run({
context,
checkoutPaymentMethod: findCheckoutPaymentMethodStrict(checkoutPaymentMethods, 'credit_card'),
nonCdeFormInputs: createInputsDictFromForm(formRef.current),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ping3DSStatusResponse, ThreeDSStatus } from '@getopenpay/utils';
import { pingCdeFor3dsStatus } from '../utils/connection';
import { pingCdeFor3dsStatus } from '../cde-client';
import { createAndOpenFrame } from './frame';

export interface PopupElements {
Expand Down
29 changes: 28 additions & 1 deletion packages/utils/src/cde-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ElementType,
FieldName,
PaymentFlowStartedEventPayload,
Ping3DSStatusResponse,
SetupCheckoutRequest,
SubmitEventPayload,
TokenizeCardRequest,
Expand All @@ -29,6 +30,7 @@ import {
import { sleep } from './stripe';
import { sum } from './math';
import { CustomError } from 'ts-custom-error';
import { connectToChild } from 'penpal';

/*
* An actual custom Error object, created from a CDEResponseError object
Expand Down Expand Up @@ -57,9 +59,11 @@ export const queryCDE = async <T extends z.ZodType>(
// 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);

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);
Expand Down Expand Up @@ -197,7 +201,12 @@ export const checkoutCardElements = async (
cdeConn: CdeConnection,
payload: CardElementsCheckoutRequest
): Promise<CheckoutSuccessResponse> => {
return await queryCDE(cdeConn, { type: 'checkout_card_elements', payload }, CheckoutSuccessResponse);
try {
return await queryCDE(cdeConn, { type: 'checkout_card_elements', payload }, CheckoutSuccessResponse);
} catch (error) {
console.error('[cde-client] Error during checkoutCardElements:', error, JSON.stringify(error));
throw error;
}
};

export const setupCheckout = async (
Expand All @@ -213,3 +222,21 @@ export const performCheckout = async (
): Promise<CheckoutSuccessResponse> => {
return await queryCDE(cdeConn, { type: 'checkout', payload }, CheckoutSuccessResponse);
};

/**
* @throws if the response is not valid or connection failed
*/
export async function pingCdeFor3dsStatus(iframe: HTMLIFrameElement, childOrigin: string) {
zeyarpaing marked this conversation as resolved.
Show resolved Hide resolved
const connection = connectToChild({
iframe,
debug: true,
timeout: 1000,
childOrigin,
});
const connectionObj = await connection.promise;
const message: CdeMessage = { type: 'ping-3ds-status' };
// @ts-expect-error `send` typing
const result = await connectionObj.send(message);
zeyarpaing marked this conversation as resolved.
Show resolved Hide resolved
const parsed = Ping3DSStatusResponse.parse(result);
return parsed.status;
}
1 change: 1 addition & 0 deletions packages/utils/src/cde_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const zStringReq = z.string().trim().min(1, { message: `Cannot be blank`
export const CDEResponseError = z.object({
cde_response_type: z.literal('error'),
message: z.string(),
headers: z.record(z.string(), z.string()).optional(),
});
export type CDEResponseError = z.infer<typeof CDEResponseError>;

Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/flows/all-flows.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CheckoutPaymentMethod } from '../shared-models';
import { OjsContext, OjsFlow } from './ojs-flow';
import { runStripeCcFlow } from './stripe/stripe-cc-flow';
import { runCommonCcFlow } from './card/common-cc-flow';
import { initStripePrFlow, runStripePrFlow } from './stripe/stripe-pr-flow';

export const findCheckoutPaymentMethodStrict = (
Expand Down Expand Up @@ -28,8 +28,8 @@ export const OjsFlows = {
// ✋ Note: For flows that require initialization, please add them to `init-flows.ts`

// Stripe
stripeCC: {
run: runStripeCcFlow,
commonCC: {
run: runCommonCcFlow,
},
stripePR: {
init: initStripePrFlow,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { start3dsVerification } from '../../3ds-elements/events';
import {
CdeError,
checkoutCardElements,
Expand All @@ -8,19 +9,21 @@ import {
tokenizeCardOnAllConnections,
} from '../../cde-client';
import { StartPaymentFlowForCCResponse } from '../../cde_models';
import { Common3DSNextActionMetadata } from '../../shared-models';
import { launchStripe3DSDialogFlow, Stripe3DSNextActionMetadata } from '../../stripe';
import { validateNonCdeFormFieldsForCC, validateTokenizeCardResults } from '../common/cc-flow-utils';
import { parseConfirmPaymentFlowResponse } from '../common/common-flow-utils';
import { addBasicCheckoutCallbackHandlers, createOjsFlowLoggers, RunOjsFlow, SimpleOjsFlowResult } from '../ojs-flow';

const { log__, err__ } = createOjsFlowLoggers('stripe-cc');
const { log__, err__ } = createOjsFlowLoggers('common-cc');
const { log__: stripeLog__ } = createOjsFlowLoggers('stripe-cc');
zeyarpaing marked this conversation as resolved.
Show resolved Hide resolved

/*
* Runs the main Stripe CC flow
* Runs the main common CC flow
*/
export const runStripeCcFlow: RunOjsFlow = addBasicCheckoutCallbackHandlers(
export const runCommonCcFlow: RunOjsFlow = addBasicCheckoutCallbackHandlers(
async ({ context, checkoutPaymentMethod, nonCdeFormInputs, flowCallbacks }): Promise<SimpleOjsFlowResult> => {
log__(`Running Stripe CC flow...`);
log__(`Running common CC flow...`);
const anyCdeConnection = Array.from(context.cdeConnections.values())[0];
const prefill = await getPrefill(anyCdeConnection);

Expand Down Expand Up @@ -53,16 +56,19 @@ export const runStripeCcFlow: RunOjsFlow = addBasicCheckoutCallbackHandlers(
if (error instanceof CdeError) {
if (error.originalErrorMessage === '3DS_REQUIRED') {
log__(`Card requires 3DS, starting non-legacy payment flow`);

const threeDSUrl = error.response.headers?.['x-3ds-auth-url'];
const startPfResult = await startPaymentFlowForCC(anyCdeConnection, commonCheckoutParams);
const nextActionMetadata = parse3DSNextActionMetadata(startPfResult);

log__(`Launching Stripe 3DS dialog flow`);
await launchStripe3DSDialogFlow(nextActionMetadata);
if (threeDSUrl) {
await commonCC3DSFlow(threeDSUrl, startPfResult);
} else {
await stripeCC3DSFlow(startPfResult);
}

// TODO ASAP: ideally we also do confirmPaymentFlow for non-setup mode,
// but for some reason 3DS_REQUIRED is thrown again during confirmPaymentFlow
// even though the 3DS flow has been completed.

if (prefill.mode === 'setup') {
log__(`Confirming payment flow`);
const confirmResult = await confirmPaymentFlow(anyCdeConnection, {
Expand Down Expand Up @@ -92,11 +98,33 @@ export const runStripeCcFlow: RunOjsFlow = addBasicCheckoutCallbackHandlers(
/*
* Parses the 3DS next action metadata from the start payment flow response
*/
const parse3DSNextActionMetadata = (response: StartPaymentFlowForCCResponse): Stripe3DSNextActionMetadata => {
const parseStripe3DSNextActionMetadata = (response: StartPaymentFlowForCCResponse): Stripe3DSNextActionMetadata => {
if (response.required_user_actions.length !== 1) {
throw new Error(
`Error occurred.\nDetails: got ${response.required_user_actions.length} required user actions. Expecting only one action`
);
}
return Stripe3DSNextActionMetadata.parse(response.required_user_actions[0]);
};

const parseCommon3DSNextActionMetadata = (response: StartPaymentFlowForCCResponse): Common3DSNextActionMetadata => {
if (response.required_user_actions.length !== 1) {
throw new Error(
`Error occurred.\nDetails: got ${response.required_user_actions.length} required user actions. Expecting only one action`
);
}
return Common3DSNextActionMetadata.parse(response.required_user_actions[0]);
};

const commonCC3DSFlow = async (threeDSUrl: string, startPfResult: StartPaymentFlowForCCResponse) => {
const status = await start3dsVerification({ url: threeDSUrl, baseUrl: 'http://localhost:8001' });
log__(`3DS verification status: ${status}`);
const nextActionMetadata = parseCommon3DSNextActionMetadata(startPfResult);
log__('nextActionMetadata', nextActionMetadata);
};

const stripeCC3DSFlow = async (startPfResult: StartPaymentFlowForCCResponse) => {
const nextActionMetadata = parseStripe3DSNextActionMetadata(startPfResult);
stripeLog__('nextActionMetadata', nextActionMetadata);
await launchStripe3DSDialogFlow(nextActionMetadata);
};
7 changes: 7 additions & 0 deletions packages/utils/src/shared-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,10 @@ export const Ping3DSStatusResponse = z.object({
});

export type Ping3DSStatusResponse = z.infer<typeof Ping3DSStatusResponse>;

export const Common3DSNextActionMetadata = z.object({
type: z.string(),
initial_intent_id: z.string(),
consent_id: z.string(),
});
export type Common3DSNextActionMetadata = z.infer<typeof Common3DSNextActionMetadata>;
17 changes: 14 additions & 3 deletions packages/utils/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ export default defineConfig({
optimizeDeps: {
include: ['react', 'react-dom', 'react/jsx-runtime', 'penpal', 'use-async-effect', 'uuid', 'zod'],
},
esbuild: {
sourcemap: 'inline',
},
build: {
copyPublicDir: false,
lib: {
formats: ['es'],
entry: resolve(__dirname, './index.ts'),
fileName: `index`,
},
// commonjsOptions: {
// include: ['react', 'react-dom', 'react/jsx-runtime', 'penpal', 'use-async-effect', 'uuid', 'zod'],
// },
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
output: {
Expand All @@ -32,6 +32,17 @@ export default defineConfig({
rollupTypes: true,
tsconfigPath: resolve(__dirname, 'tsconfig.build.json'),
}),
{
name: 'css-inline',
transform(code, id) {
if (id.endsWith('.css?inline')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
};
}
},
}, // To inline CSS into the bundle
],
cacheDir: './.vite',
});
13 changes: 5 additions & 8 deletions packages/vanilla/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,11 @@ export class OpenPayFormEventHandler {

async handleLoadedEvent(source: MessageEventSource, elementId: string, payload: LoadedEventPayload) {
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;
// console.log('handleLoadedEvent XXXXXXXXX', payload);
// this.formInstance.onCdeLoaded(payload);
// if (this.config.onLoad) {
// this.config.onLoad(payload.totalAmountAtoms, payload.currency);
// }
this.eventTargets[elementId] = source;
this.formInstance.onCdeLoaded(payload);
if (this.config.onLoad) {
this.config.onLoad(payload.totalAmountAtoms, payload.currency);
}
}

handleLoadErrorEvent(payload: ErrorEventPayload) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class OpenPayForm {

submit() {
const context = this.createOjsFlowContext();
OjsFlows.stripeCC.run({
OjsFlows.commonCC.run({
context,
checkoutPaymentMethod: findCheckoutPaymentMethodStrict(context.checkoutPaymentMethods, 'credit_card'),
nonCdeFormInputs: this.getNonCdeFormInputs(),
Expand Down
12 changes: 1 addition & 11 deletions packages/vanilla/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dts from 'vite-plugin-dts';
export default defineConfig(({ mode }) => {
return {
esbuild: {
sourcemap: 'inline',
drop: mode === 'development' ? [] : ['console', 'debugger'],
},
optimizeDeps: {
Expand Down Expand Up @@ -33,17 +34,6 @@ export default defineConfig(({ mode }) => {
bundledPackages: ['@getopenpay/utils'],
tsconfigPath: resolve(__dirname, 'tsconfig.build.json'),
}),
{
name: 'css-inline',
transform(code, id) {
if (id.endsWith('.css?inline')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
};
}
},
}, // To inline CSS into the bundle
],
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
Expand Down