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

✨🔗 [OJS] All new stripe link #74

Merged
merged 10 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
5 changes: 3 additions & 2 deletions apps/vanilla-example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ <h1 class="title">OpenPay.js</h1>
<button id="apple-pay-button" class="submit-button" style="display: none" type="button">Apple Pay</button>
<!-- Google pay button -->
<button id="google-pay-button" class="submit-button" style="display: none" type="button">Google Pay</button>
<!-- Stripe link button -->
<button id="stripe-link-button" class="submit-button" style="display: none" type="button">Stripe Link</button>
<!-- Stripe link -->
<div id="ojs-stripe-link-btn" class="stripe-link-button"></div>

<div id="validation-error-container" class="error-message"></div>
</div>

Expand Down
4 changes: 4 additions & 0 deletions apps/vanilla-example/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ input {
cursor: not-allowed;
}

.stripe-link-button {
margin-top: calc(var(--spacing) * 1.4);
}

@keyframes spin {
from {
transform: rotate(0deg);
Expand Down
3 changes: 2 additions & 1 deletion packages/react/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ const ElementsForm: FC<ElementsFormProps> = (props) => {
elementsSessionId: sessionId,
checkoutPaymentMethods,
cdeConnections,
customInitParams: {},
baseUrl: new URL(frameBaseUrl).origin,
};
return context;
Expand All @@ -428,7 +429,7 @@ const ElementsForm: FC<ElementsFormProps> = (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') {
Expand Down
17 changes: 9 additions & 8 deletions packages/utils/src/cde-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import {
ConfirmPaymentFlowResponse,
ElementType,
FieldName,
PaymentFlowStartedEventPayload,
Ping3DSStatusResponse,
SetupCheckoutRequest,
SubmitEventPayload,
TokenizeCardRequest,
TokenizeCardResponse,
} from './shared-models';
Expand All @@ -25,6 +23,7 @@ import {
StartPaymentFlowForCCRequest,
StartPaymentFlowForCCResponse,
StartPaymentFlowForPRRequest,
StartPaymentFlowRequest,
StartPaymentFlowResponse,
} from './cde_models';
import { sleep } from './stripe';
Expand All @@ -33,9 +32,11 @@ import { CustomError } from 'ts-custom-error';
import { connectToChild } from 'penpal';

/*
* 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;
Expand Down Expand Up @@ -97,9 +98,9 @@ export const getPrefill = async (cdeConn: CdeConnection): Promise<PaymentFormPre

export const startPaymentFlow = async (
cdeConn: CdeConnection,
payload: SubmitEventPayload
): Promise<PaymentFlowStartedEventPayload> => {
return await queryCDE(cdeConn, { type: 'start_payment_flow', payload }, PaymentFlowStartedEventPayload);
payload: StartPaymentFlowRequest
): Promise<StartPaymentFlowResponse> => {
return await queryCDE(cdeConn, { type: 'v2_start_payment_flow', payload }, StartPaymentFlowResponse);
};

export const startPaymentFlowForCC = async (
Expand Down
11 changes: 11 additions & 0 deletions packages/utils/src/cde_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,17 @@ export type PydanticValidationError = z.infer<typeof PydanticValidationError>;
export const PydanticValidationErrorResponse = z.array(PydanticValidationError);
export type PydanticValidationErrorResponse = z.infer<typeof PydanticValidationErrorResponse>;

// StartPaymentFlowRequest
export const StartPaymentFlowRequest = z.object({
new_customer_email: z.string().optional(),
new_customer_address: z.record(z.string(), z.any()).optional(),
zeyarpaing marked this conversation as resolved.
Show resolved Hide resolved
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<typeof StartPaymentFlowRequest>;

// StartPaymentFlowResponse
export const StartPaymentFlowResponse = z.object({
required_user_actions: z.array(z.record(z.string(), z.any())),
Expand Down
9 changes: 8 additions & 1 deletion packages/utils/src/flows/all-flows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheckoutPaymentMethod } from '../shared-models';
import { OjsContext, OjsFlow } from './ojs-flow';
import { initStripeLinkFlow, runStripeLinkFlow } from './stripe/stripe-link-flow';
import { runCommonCcFlow } from './card/common-cc-flow';
import { initStripePrFlow, runStripePrFlow } from './stripe/stripe-pr-flow';

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

// Stripe
// Common
commonCC: {
run: runCommonCcFlow,
},

// Stripe
stripePR: {
init: initStripePrFlow,
run: runStripePrFlow,
},
stripeLink: {
init: initStripeLinkFlow,
run: runStripeLinkFlow,
},

// 👉 Add more flows here

Expand Down
19 changes: 13 additions & 6 deletions packages/utils/src/flows/common/common-flow-utils.ts
Original file line number Diff line number Diff line change
@@ -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<T> =
| {
Expand Down Expand Up @@ -33,10 +33,17 @@ export const parseConfirmPaymentFlowResponse = (
export const findCpmMatchingType = <T>(allCPMs: CheckoutPaymentMethod[], zodModel: z.ZodSchema<T>): 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<string, unknown>): Record<string, unknown> => {
const newFormInputs = { ...formInputs };
if (!newFormInputs[FieldName.ZIP_CODE]) {
log__(`Overriding empty zip code`);
newFormInputs[FieldName.ZIP_CODE] = '00000';
}
return newFormInputs;
};
113 changes: 113 additions & 0 deletions packages/utils/src/flows/foobar-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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.
// - 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
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<typeof FoobarCpm>;

// 👉 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 (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<InitFoobarFlowSuccess> = addErrorCatcherForInit(
async ({ context }): Promise<InitFoobarFlowSuccess> => {
// 👉 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}`);

// 👉 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<FoobarFlowCustomParams, InitFoobarFlowSuccess> =
addBasicCheckoutCallbackHandlers(
async ({
context,
checkoutPaymentMethod,
nonCdeFormInputs,
flowCallbacks,
customParams,
initResult,
}): Promise<SimpleOjsFlowResult> => {
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);
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: '',
},
};
}
);
10 changes: 7 additions & 3 deletions packages/utils/src/flows/init-flows.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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
};
Expand Down
14 changes: 14 additions & 0 deletions packages/utils/src/flows/ojs-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -105,6 +110,15 @@ export type OjsContext = {
*/
cdeConnections: Map<ElementType, CdeConnection>;

/*
* Custom init params
*/
customInitParams: {
stripeLink?: {
buttonHeight?: number;
};
};

/**
* The base URL of the CDE iframe.
*/
Expand Down
Loading