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

⚓️ Stripe link hooks #78

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 32 additions & 3 deletions apps/react-example/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Form: FC<FormProps> = (props) => {
} | null>(null);

const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({});
const [stripeLinkShown, setStripeLinkShown] = useState<boolean>(true);

const prParams = {
overridePaymentRequest: {
Expand Down Expand Up @@ -106,10 +107,11 @@ const Form: FC<FormProps> = (props) => {
customInitParams={{
stripeLink: {
overrideLinkSubmit: async () => true,
doNotMountOnInit: false,
},
}}
>
{({ submit, applePay, googlePay, loaded }) => (
{({ submit, applePay, googlePay, loaded, stripeLink }) => (
<FormWrapper error={validationErrors}>
{loading && (
<div data-testid="loading" className="flex items-center">
Expand Down Expand Up @@ -186,8 +188,35 @@ const Form: FC<FormProps> = (props) => {
>
{googlePay.isLoading ? 'Loading' : 'Google Pay'}
</button>

<div id="ojs-stripe-link-btn" className="stripe-link-button mt-2"></div>
{
stripeLinkShown ?
<div id="ojs-stripe-link-btn" className="stripe-link-button mt-2">Loading...</div>
:
<></>
}
<div
className="grid grid-cols-2 mt-8 bg-gray-100 p-4 rounded-lg drop-shadow"
>
{
stripeLink &&
<button
onClick={async () => {
if (stripeLinkShown) {
setStripeLinkShown(false);
stripeLink.dismountButton();
}
else {
setStripeLinkShown(true);
await stripeLink.waitForButtonToMount();
stripeLink.mountButton()
}
}}
className='text-white text-sm px-2 py-1 w-full rounded-md bg-gray-600 hover:bg-gray-700 active:bg-gray-800'
>
{stripeLinkShown ? 'Hide' : 'Show'} stripe link
</button>
}
</div>
</FormWrapper>
)}
</ElementsForm>
Expand Down
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.

80 changes: 8 additions & 72 deletions packages/react/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -80,6 +81,7 @@ const ElementsForm: FC<ElementsFormProps> = (props) => {
apple_pay: PR_LOADING,
google_pay: PR_LOADING,
});
const [stripeLinkCtrl, setStripeLinkCtrl] = useState<StripeLinkController | null>(null);

useEffect(() => {
const ojs_version = { version: __APP_VERSION__, release_version: __RELEASE_VERSION__ };
Expand Down Expand Up @@ -462,6 +464,11 @@ const ElementsForm: FC<ElementsFormProps> = (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]);
Expand Down Expand Up @@ -530,49 +537,6 @@ const ElementsForm: FC<ElementsFormProps> = (props) => {
[iframes]
);

// const onUserCompletePaymentRequestUI = async (
// stripePm: PaymentRequestPaymentMethodEvent | null,
// checkoutPaymentMethod: CheckoutPaymentMethod
// ): Promise<void> => {
// 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,
Expand All @@ -582,41 +546,13 @@ const ElementsForm: FC<ElementsFormProps> = (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 (
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -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 <info@getopenpay.com> (https://getopenpay.com)",
"private": false,
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/flows/init-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -71,3 +72,4 @@ const runInitFlowAsObservable = <T extends InitOjsFlowResult>(
};

export type OjsFlowsInitialization = ReturnType<typeof initializeOjsFlows>;
export type { StripeLinkController };
6 changes: 6 additions & 0 deletions packages/utils/src/flows/ojs-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ export type CustomInitParams = {
* Note that this function must complete within 1 second, or the submission will fail.
*/
overrideLinkSubmit?: () => Promise<boolean>;

/**
* 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;
};
};

Expand Down
68 changes: 60 additions & 8 deletions packages/utils/src/flows/stripe/stripe-link-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ type RunStripeLinkFlowParams = {
stripePM: PaymentMethod;
};

export type StripeLinkController = {
mountButton: () => void;
dismountButton: () => void;
waitForButtonToMount: () => Promise<HTMLElement>;
};

export type InitStripeLinkFlowResult =
| {
isAvailable: true;
controller: StripeLinkController;
}
| {
isAvailable: false;
};

export const StripeLinkCpm = z.object({
provider: z.literal('stripe_link'),
processor_name: z.literal('stripe'),
Expand All @@ -53,8 +68,10 @@ export type StripeLinkRequiredUserActions = z.infer<typeof StripeLinkRequiredUse
/*
* Initializes the Stripe link flow (put more details here)
*/
export const initStripeLinkFlow: InitOjsFlow<InitOjsFlowResult> = addErrorCatcherForInit(
async ({ context, flowCallbacks }): Promise<InitOjsFlowResult> => {
export const initStripeLinkFlow: InitOjsFlow<InitStripeLinkFlowResult> = addErrorCatcherForInit(
async ({ context, flowCallbacks }): Promise<InitStripeLinkFlowResult> => {
const initParams = context.customInitParams.stripeLink;

log__(`Checking if there are any CPMs for Stripe PR...`);
const stripeLinkCpm = findCpmMatchingType(context.checkoutPaymentMethods, StripeLinkCpm);

Expand All @@ -72,21 +89,36 @@ export const initStripeLinkFlow: InitOjsFlow<InitOjsFlowResult> = 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',
googlePay: 'never',
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;
Expand Down Expand Up @@ -118,7 +150,14 @@ export const initStripeLinkFlow: InitOjsFlow<InitOjsFlowResult> = addErrorCatche
}
});

return { isAvailable: true };
return {
isAvailable: true,
controller: {
mountButton,
dismountButton,
waitForButtonToMount: async () => await getElementByIdAsync(OJS_STRIPE_LINK_BTN_ID),
},
};
}
);

Expand Down Expand Up @@ -209,3 +248,16 @@ const fillEmptyFormInputsWithStripePM = (
log__(`Final form inputs:`, inputs);
return inputs;
};

const getElementByIdAsync = (id: string) =>
new Promise<HTMLElement>((resolve) => {
const getElement = () => {
const element = document.getElementById(id);
if (element) {
resolve(element);
} else {
requestAnimationFrame(getElement);
}
};
getElement();
});
7 changes: 2 additions & 5 deletions packages/utils/src/models.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,11 +17,7 @@ export type ElementsFormChildrenProps = {
submit: () => void;
applePay: PaymentRequestStatus;
googlePay: PaymentRequestStatus;
// stripeLink: {
// button: FC<StripeLinkButtonProps>;
// authElement: FC<LinkAuthElementProps>;
// pr: PaymentRequestStatus;
// };
stripeLink: StripeLinkController | null;
loaded: boolean;
preview: DynamicPreview;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla/package.json
Original file line number Diff line number Diff line change
@@ -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 <info@getopenpay.com> (https://getopenpay.com)",
"license": "ISC",
Expand Down
Loading