Files
Jyotisha/frontend/src/lib/onboarding-state.ts
T
jesse-uxandGrok (agent loop) fdd592d3f3 feat: 新账户首问免费,档案可标明关系
只做启程(一):同一笔预扣覆盖已公布的标准档,更贵的只扣差价;引导状态与人物关系只加列。tsc 与 lint 通过。test:db 91 项,失败 14,其中 13 条为 Windows 已知失败;校正遥测单独重跑通过。

Co-Authored-By: Grok (agent loop)
2026-10-23 00:34:28 +08:00

93 lines
3.4 KiB
TypeScript

/** Server-owned onboarding flags. The only keys the checklist and hints may store. */
export const ONBOARDING_STATE_KEYS = ["checklistDismissedAt", "hintsSeen", "chartVisitedAt"] as const;
export const ONBOARDING_HINT_IDS = [
"follow-up",
"sidebar-chart",
"tab-chart",
"tab-info",
"tab-dasha",
"tab-west",
"syn-ready",
"ephemeris",
] as const;
export type OnboardingHintId = (typeof ONBOARDING_HINT_IDS)[number];
export type OnboardingState = {
readonly checklistDismissedAt?: string | null;
readonly hintsSeen?: readonly OnboardingHintId[];
readonly chartVisitedAt?: string | null;
};
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
function isHintId(value: unknown): value is OnboardingHintId {
return ONBOARDING_HINT_IDS.some((id) => id === value);
}
function timestamp(value: unknown): string | null | undefined {
if (value == null) return null;
if (typeof value !== "string" || !TIMESTAMP.test(value) || Number.isNaN(Date.parse(value))) return undefined;
return value;
}
export function parseOnboardingState(value: unknown): OnboardingState {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const record = value as Record<string, unknown>;
const state: {
checklistDismissedAt?: string | null;
hintsSeen?: OnboardingHintId[];
chartVisitedAt?: string | null;
} = {};
if ("checklistDismissedAt" in record) {
const parsed = timestamp(record.checklistDismissedAt);
if (parsed !== undefined) state.checklistDismissedAt = parsed;
}
if ("chartVisitedAt" in record) {
const parsed = timestamp(record.chartVisitedAt);
if (parsed !== undefined) state.chartVisitedAt = parsed;
}
if (Array.isArray(record.hintsSeen)) {
const hints = record.hintsSeen.filter(isHintId);
if (hints.length === record.hintsSeen.length) state.hintsSeen = hints;
}
return state;
}
export function parseOnboardingPatch(
value: unknown,
): { ok: true; patch: OnboardingState } | { ok: false; error: string } {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { ok: false, error: "引导状态格式不正确" };
}
const record = value as Record<string, unknown>;
const unknownKey = Object.keys(record).find((key) => !ONBOARDING_STATE_KEYS.some((allowed) => allowed === key));
if (unknownKey) return { ok: false, error: "只能记下清单、提示和是否进过星盘页" };
if (Object.keys(record).length === 0) return { ok: false, error: "没有要保存的引导状态" };
const patch: {
checklistDismissedAt?: string | null;
hintsSeen?: OnboardingHintId[];
chartVisitedAt?: string | null;
} = {};
if ("checklistDismissedAt" in record) {
const parsed = timestamp(record.checklistDismissedAt);
if (parsed === undefined) return { ok: false, error: "时间格式不正确" };
patch.checklistDismissedAt = parsed;
}
if ("chartVisitedAt" in record) {
const parsed = timestamp(record.chartVisitedAt);
if (parsed === undefined) return { ok: false, error: "时间格式不正确" };
patch.chartVisitedAt = parsed;
}
if ("hintsSeen" in record) {
if (!Array.isArray(record.hintsSeen) || record.hintsSeen.some((item) => !isHintId(item))) {
return { ok: false, error: "这个提示不在名单里" };
}
patch.hintsSeen = record.hintsSeen.filter(isHintId);
}
return { ok: true, patch };
}