feat(tip): support dynamic gift products

This commit is contained in:
2026-07-21 13:19:45 +08:00
parent 55cb98ed14
commit 37ff69020b
62 changed files with 2325 additions and 1085 deletions
+130 -38
View File
@@ -1,50 +1,76 @@
import type { DoneActorEvent, ErrorActorEvent } from "xstate";
import type { PaymentPlansResponse } from "@/data/schemas/payment";
import type {
GiftProductsResponse,
PaymentPlansResponse,
} from "@/data/schemas/payment";
import {
consumeFirstRechargeState,
hydrateGiftProductsState,
hydratePlansResponseState,
refreshPlansResponseState,
selectPlanState,
} from "../helper/catalog";
import {
resetOrderState,
toPaymentErrorMessage,
} from "../helper/order";
} from "../helper";
import { resetOrderState, toPaymentErrorMessage } from "../helper/order";
import type { PaymentEvent } from "../payment-events";
import type { PaymentState } from "../payment-state";
import {
basePaymentMachineSetup,
createPaymentActorActionSetup,
} from "./setup";
function initializeCatalogState(
context: PaymentState,
event: Extract<PaymentEvent, { type: "PaymentInit" }>,
): Partial<PaymentState> {
const planCatalog = event.catalog ?? context.planCatalog;
const giftCharacterId =
planCatalog === "tip"
? (event.characterId ?? context.giftCharacterId)
: null;
const catalogChanged = planCatalog !== context.planCatalog;
const characterChanged = giftCharacterId !== context.giftCharacterId;
const selectionChanged =
planCatalog === "tip" &&
((event.category ?? null) !== context.selectedGiftCategory ||
(event.planId ?? null) !== (context.selectedPlanId || null));
return {
planCatalog,
...(event.payChannel ? { payChannel: event.payChannel } : {}),
giftCharacterId,
requestedGiftCategory:
planCatalog === "tip" ? (event.category ?? null) : null,
requestedGiftPlanId:
planCatalog === "tip" ? (event.planId ?? null) : null,
...(catalogChanged || characterChanged || selectionChanged
? {
...resetOrderState(),
plans: [],
giftCategories: [],
giftProducts: [],
selectedGiftCategory: null,
selectedPlanId: "",
isFirstRecharge: false,
autoRenew: planCatalog !== "tip",
}
: {}),
};
}
const initializeCatalogAction = basePaymentMachineSetup.assign(
({ context, event }) => {
if (event.type !== "PaymentInit") return {};
return {
planCatalog: event.catalog ?? context.planCatalog,
...(event.payChannel ? { payChannel: event.payChannel } : {}),
};
},
({ context, event }) =>
event.type === "PaymentInit"
? initializeCatalogState(context, event)
: {},
);
const refreshCatalogAction = basePaymentMachineSetup.assign(
({ context, event }) => {
if (event.type !== "PaymentInit") return {};
const planCatalog = event.catalog ?? context.planCatalog;
const catalogChanged = planCatalog !== context.planCatalog;
return {
planCatalog,
...(event.payChannel ? { payChannel: event.payChannel } : {}),
...(catalogChanged
? {
plans: [],
selectedPlanId: "",
isFirstRecharge: false,
autoRenew: planCatalog !== "tip",
}
: {}),
};
},
({ context, event }) =>
event.type === "PaymentInit"
? initializeCatalogState(context, event)
: {},
);
const selectPlanAction = basePaymentMachineSetup.assign(
@@ -105,6 +131,12 @@ export const catalogMachineSetup = basePaymentMachineSetup.extend({
resetOrder: resetOrderAction,
consumeFirstRecharge: consumeFirstRechargeAction,
},
guards: {
isTipInit: ({ context, event }) =>
event.type === "PaymentInit" &&
(event.catalog ?? context.planCatalog) === "tip",
isTipCatalog: ({ context }) => context.planCatalog === "tip",
},
});
const cachedPlansDoneSetup =
@@ -113,6 +145,8 @@ const cachedPlansDoneSetup =
>();
const plansDoneSetup =
createPaymentActorActionSetup<DoneActorEvent<PaymentPlansResponse>>();
const giftProductsDoneSetup =
createPaymentActorActionSetup<DoneActorEvent<GiftProductsResponse>>();
const plansErrorSetup = createPaymentActorActionSetup<ErrorActorEvent>();
const hydrateCachedPlansAction = cachedPlansDoneSetup.assign(
@@ -130,16 +164,42 @@ const refreshPlansAction = plansDoneSetup.assign(({ context, event }) =>
refreshPlansResponseState(event.output, context.selectedPlanId),
);
const hydrateGiftProductsAction = giftProductsDoneSetup.assign(
({ context, event }) =>
hydrateGiftProductsState(
event.output,
context.giftCharacterId ?? "",
context.requestedGiftCategory ?? context.selectedGiftCategory,
context.requestedGiftPlanId ?? context.selectedPlanId,
),
);
const applyPlansErrorAction = plansErrorSetup.assign(({ event }) => ({
errorMessage: toPaymentErrorMessage(event.error),
}));
const applyGiftProductsErrorAction = plansErrorSetup.assign(({ event }) => ({
plans: [],
giftCategories: [],
giftProducts: [],
selectedGiftCategory: null,
selectedPlanId: "",
errorMessage: toPaymentErrorMessage(event.error),
}));
export const idleState = catalogMachineSetup.createStateConfig({
on: {
PaymentInit: {
target: "loadingCachedPlans",
actions: "initializeCatalog",
},
PaymentInit: [
{
guard: "isTipInit",
target: "loadingGiftProducts",
actions: "initializeCatalog",
},
{
target: "loadingCachedPlans",
actions: "initializeCatalog",
},
],
},
});
@@ -190,12 +250,44 @@ export const refreshingPlansState = catalogMachineSetup.createStateConfig({
},
});
export const loadingGiftProductsState =
catalogMachineSetup.createStateConfig({
invoke: {
src: "loadGiftProducts",
input: ({ context }) => ({
characterId: context.giftCharacterId ?? "",
}),
onDone: {
target: "ready",
actions: hydrateGiftProductsAction,
},
onError: {
target: "ready",
actions: applyGiftProductsErrorAction,
},
},
});
export const readyState = catalogMachineSetup.createStateConfig({
on: {
PaymentInit: {
target: "refreshingPlans",
actions: "refreshCatalog",
},
PaymentInit: [
{
guard: "isTipInit",
target: "loadingGiftProducts",
actions: "refreshCatalog",
},
{
target: "refreshingPlans",
actions: "refreshCatalog",
},
],
PaymentCatalogRetryRequested: [
{
guard: "isTipCatalog",
target: "loadingGiftProducts",
},
{ target: "refreshingPlans" },
],
PaymentPlanSelected: { actions: "selectPlan" },
PaymentPayChannelChanged: { actions: "changePayChannel" },
PaymentAutoRenewChanged: { actions: "changeAutoRenew" },