Skip to content

Commit

Permalink
⚓️ Stripe link hooks (#78)
Browse files Browse the repository at this point in the history
* impl

* bumpver

* Cleanup
  • Loading branch information
syvlabs authored Dec 3, 2024
1 parent 83dddf0 commit 04f5f70
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 92 deletions.
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

0 comments on commit 04f5f70

Please sign in to comment.