UNPKG

@daimo/pay

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

215 lines (212 loc) 7.56 kB
import { assert, DaimoPayOrderMode, DaimoPayIntentStatus, isHydrated } from '@daimo/pay-common'; import { parseUnits } from 'viem'; const initialPaymentState = { type: "idle" }; /** * Master payment reducer. */ function paymentReducer(state, event) { switch (state.type) { case "idle": return reduceIdle(state, event); case "preview": return reducePreview(state, event); case "unhydrated": return reduceUnhydrated(state, event); case "payment_unpaid": return reducePaymentUnpaid(state, event); case "payment_started": return reducePaymentStarted(state, event); case "payment_completed": case "payment_bounced": case "error": return reduceTerminal(state, event); /* satisfies exhaustiveness */ default: // Exhaustive check: Using `never` will cause lint failure if not all // state types are handled const _exhaustive = state; return _exhaustive; } } /* -------------------------------------------------- reducer helpers – one function per state ---------------------------------------------------*/ function reduceIdle(state, event) { switch (event.type) { case "preview_generated": { const stateFromOrder = getStateFromOrder(event.order); // If order is already hydrated or in terminal state, use that state if (stateFromOrder.type !== "unhydrated") { return stateFromOrder; } // Order is not hydrated/processed, handle as preview return { type: "preview", order: event.order, payParamsData: event.payParamsData, }; } case "order_loaded": { return getStateFromOrder(event.order); } case "error": return { type: "error", order: event.order, message: event.message, }; case "reset": return initialPaymentState; default: return state; } } function reducePreview(state, event) { assert(state.order.mode !== DaimoPayOrderMode.HYDRATED, "reducePreview called on hydrated order"); switch (event.type) { case "order_hydrated": return { type: "payment_unpaid", order: event.order }; case "set_chosen_usd": { const token = state.order.destFinalCallTokenAmount.token; const tokenUnits = (event.usd / token.priceFromUsd).toString(); const tokenAmount = parseUnits(tokenUnits, token.decimals); // Stay in preview state, but update the order's destFinalCallTokenAmount return { type: "preview", order: { ...state.order, destFinalCallTokenAmount: { token, amount: tokenAmount.toString(), usd: event.usd, }, }, payParamsData: state.payParamsData, }; } case "error": return { type: "error", order: event.order, message: event.message, }; case "reset": return initialPaymentState; default: return state; } } function reduceUnhydrated(state, event) { switch (event.type) { case "order_hydrated": return { type: "payment_unpaid", order: event.order }; case "error": return { type: "error", order: event.order, message: event.message, }; case "reset": return initialPaymentState; default: return state; } } function reducePaymentUnpaid(state, event) { switch (event.type) { case "payment_verified": { if (event.order.intentStatus === DaimoPayIntentStatus.UNPAID) { // The payment was not detected on chain, or some other error occurred. return { type: "error", order: event.order, message: "Payment failed", }; } return getStateFromHydratedOrder(state, event.order); } case "order_refreshed": return getStateFromHydratedOrder(state, event.order); case "error": return { type: "error", order: event.order, message: event.message, }; case "reset": return initialPaymentState; default: return state; } } function reducePaymentStarted(state, event) { switch (event.type) { case "order_refreshed": return getStateFromHydratedOrder(state, event.order); case "error": return { type: "error", order: event.order, message: event.message, }; case "reset": return initialPaymentState; default: return state; } } /** * Determines the appropriate payment state based on an order's status and mode. * Returns the appropriate payment state based on the order's mode and intent status. */ function getStateFromOrder(order) { if (order.intentStatus === DaimoPayIntentStatus.COMPLETED) { assert(order.mode === DaimoPayOrderMode.HYDRATED, `[PAYMENT_REDUCER] order ${order.id} is ${order.intentStatus} but not hydrated`); return { type: "payment_completed", order }; } else if (order.intentStatus === DaimoPayIntentStatus.BOUNCED) { assert(order.mode === DaimoPayOrderMode.HYDRATED, `[PAYMENT_REDUCER] order ${order.id} is ${order.intentStatus} but not hydrated`); return { type: "payment_bounced", order }; } else if (order.intentStatus === DaimoPayIntentStatus.STARTED) { assert(order.mode === DaimoPayOrderMode.HYDRATED, `[PAYMENT_REDUCER] order ${order.id} is ${order.intentStatus} but not hydrated`); return { type: "payment_started", order }; } else if (order.mode === DaimoPayOrderMode.HYDRATED) { return { type: "payment_unpaid", order }; } else { // Order is not hydrated (SALE or CHOOSE_AMOUNT mode) return { type: "unhydrated", order }; } } /** * Determines the appropriate payment state for a hydrated order. Progresses * the payment through different processing states. */ function getStateFromHydratedOrder(state, order) { assert(isHydrated(order), `[PAYMENT_REDUCER] unhydrated`); switch (order.intentStatus) { case DaimoPayIntentStatus.UNPAID: return { type: "payment_unpaid", order }; case DaimoPayIntentStatus.STARTED: return { type: "payment_started", order }; case DaimoPayIntentStatus.COMPLETED: return { type: "payment_completed", order }; case DaimoPayIntentStatus.BOUNCED: return { type: "payment_bounced", order }; default: return state; } } function reduceTerminal(state, event) { switch (event.type) { case "reset": return initialPaymentState; // In terminal states we ignore everything except reset default: return state; } } export { initialPaymentState, paymentReducer }; //# sourceMappingURL=paymentFsm.js.map