@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
JavaScript
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