"use client"; import { useState, type Dispatch, FormEvent, MutableRefObject, SetStateAction } from "react"; import { beamAvatarSchema, type BeamAvatarPatch, } from "@/lib/beam-avatar"; import { isBirthTimeDraftReady, } from "@/lib/birth-time-intake-model"; import { type LatestAccountRequestGuard, } from "@/lib/birth-time-consultation-consent"; import { readChartBirthFingerprint } from "@/lib/chart-birth-truth"; import { composerDraftSnapshot } from "@/lib/composer-draft"; import { fetchAccount, friendlyError, LoginRedirectError, } from "@/lib/home-cloud-sync"; import { accountChartPinStillCurrent, beginChartCacheAccountRead, pinChartSnapshotIdentity, } from "@/lib/secondary-page-data"; import { missingProfileStep, readProfile, selectedBirthPlace, } from "@/lib/home-profile"; import { notifyError, notifySuccess } from "@/lib/notify"; import { saveSelfProfile } from "@/lib/self-profile-save"; import { createStartGreeting } from "@/lib/starter-greeting"; import { clearHomeWarmSnapshot } from "@/lib/home-warm-snapshot"; import { type Account, type AccountDialog, type OnboardingStep, type OpenAccountDialogOptions, type Profile, accountDialogOptions, emptyProfile, } from "@/lib/home-types"; import { preserveShallowEqual } from "@/lib/preserve-shallow-equal"; import { requestBirthTimeAssessment, type JourneyClientResponse, } from "@/lib/birth-time-journey-client"; import { previewRectificationJourney } from "@/lib/birth-time-guided-preview"; import { selfHostedOtpActions } from "@/modules/identity/client"; import type { BirthTimeAssessmentPhase } from "@/components/birth-time-assessment-overlay"; export type ProfileOnboardingParams = { account: Account | null; accountRefreshGuard: MutableRefObject; accountTrigger: MutableRefObject; birthTimeRevisionPendingRef: MutableRefObject; dialogReturnTargetRef: MutableRefObject; setAccount: Dispatch>; setAccountError: Dispatch>; setAccountMenuOpen: Dispatch>; setActiveAccountDialog: Dispatch>; setBirthTimeAssessmentPhase: Dispatch>; setBirthTimeError: Dispatch>; setBirthTimeJourney: Dispatch>; setBillingPane: Dispatch>; setDraft: (value: string) => void; setOnboardingJustCompleted: Dispatch>; setOnboardingStep: Dispatch>; setPresetMessageLength: Dispatch>; setSigningOut: Dispatch>; setStartGreeting: Dispatch>; signingOut: boolean; uiPreview: MutableRefObject; /** Warm return to `/` (BUG-1040): the profile Home starts with instead of empty. */ initialProfile?: () => Profile | null; /** The reader's own birth place was saved in onboarding (first chart): 「那一刻的天空」 reveal. */ onOwnBirthPlaceSaved?: () => void; }; export function useProfileOnboarding(params: ProfileOnboardingParams) { const { account, accountRefreshGuard, accountTrigger, birthTimeRevisionPendingRef, dialogReturnTargetRef, setAccount, setAccountError, setAccountMenuOpen, setActiveAccountDialog, setBirthTimeAssessmentPhase, setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft, setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength, setSigningOut, setStartGreeting, signingOut, uiPreview, initialProfile, onOwnBirthPlaceSaved, } = params; // Owned here since 2026-09-16 (state lowering batch 2). Home reads them back // from this hook's return instead of declaring them itself. const [profile, setProfile] = useState(() => initialProfile?.() ?? emptyProfile); const [profileDraft, setProfileDraft] = useState(() => initialProfile?.() ?? emptyProfile); const [profileNotice, setProfileNotice] = useState(""); const [avatarSaving, setAvatarSaving] = useState(false); const [profileSaving, setProfileSaving] = useState(false); async function refreshAccount() { const requestIdentity = accountRefreshGuard.current.begin(); const chartRead = beginChartCacheAccountRead(); try { const latest = await fetchAccount(); if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return; const fingerprint = readChartBirthFingerprint(latest); if (fingerprint && accountChartPinStillCurrent(chartRead)) { pinChartSnapshotIdentity({ accountId: latest.user.id, fingerprint }); } const nextProfile = readProfile(latest.profile); setProfile((current) => preserveShallowEqual(current, nextProfile)); setAccount(latest); setAccountError(""); } catch (caught) { if (caught instanceof LoginRedirectError) return; if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return; setAccountError(caught instanceof Error ? caught.message : "暂时无法读取账户信息"); } } function openAccountDialog( dialog: AccountDialog, returnTargetOrOptions?: HTMLButtonElement | null | OpenAccountDialogOptions, ) { const options = accountDialogOptions(returnTargetOrOptions); dialogReturnTargetRef.current = options.returnTarget ?? accountTrigger.current; setAccountMenuOpen(false); setAccountError(""); if (dialog === "profile") { setProfileNotice(""); } if (dialog === "chart-library") { window.location.assign("/people"); return; } if (dialog === "billing") { setBillingPane({ source: options.source, plan: options.plan, tab: options.tab, }); } setActiveAccountDialog(dialog); } function closeAccountDialog() { if (signingOut) return; setActiveAccountDialog(null); const returnTarget = dialogReturnTargetRef.current; window.requestAnimationFrame(() => returnTarget?.focus()); } async function persistAvatar(patch: BeamAvatarPatch) { if (!account?.avatar || avatarSaving) return; setAvatarSaving(true); try { const response = await fetch("/api/account/avatar", { method: "PATCH", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify(patch), }); const payload = await response.json().catch(() => null) as { avatar?: unknown; error?: string } | null; if (!response.ok) throw new Error(payload?.error || "头像暂时无法保存。"); const avatar = beamAvatarSchema.parse(payload?.avatar); setAccount((current) => current ? { ...current, avatar } : current); notifySuccess(patch.action === "randomize" ? "已生成并保存新头像。" : "头像配色已保存。"); } catch (caught) { notifyError(friendlyError(caught instanceof Error ? caught.message : "头像保存失败")); } finally { setAvatarSaving(false); } } async function persistProfile(nextProfile: Profile): Promise { if (!account) throw new Error("账户尚未加载完成"); if (process.env.NODE_ENV === "development" && uiPreview.current) return nextProfile; // One write path for the owner's birth data, shared with 星盘档案 (BUG-1081). const { profile: savedProfile, fingerprint } = await saveSelfProfile(nextProfile); if (fingerprint) pinChartSnapshotIdentity({ accountId: account.user.id, fingerprint }); setAccount((current) => current ? { ...current, profile: savedProfile } : current); return savedProfile; } async function assessSavedBirthTime(nextProfile: Profile) { const result = process.env.NODE_ENV === "development" && uiPreview.current ? previewRectificationJourney : await requestBirthTimeAssessment(); const nextStatus = result.snapshot.state === "ready" ? "confirmed" : result.snapshot.state === "candidate" ? "candidate" : "rectifying"; const assessedProfile: Profile = { ...nextProfile, time: result.snapshot.activeTime ?? "", birthTimeStatus: nextStatus, rectificationCaseId: result.caseId, }; setBirthTimeJourney(result); setBirthTimeError(""); setProfile(assessedProfile); setProfileDraft(assessedProfile); return assessedProfile; } async function saveOnboardingName() { const name = composerDraftSnapshot().replace(/\s+/g, " ").trim().slice(0, 80); if (!name || !account || profileSaving) return; const nextProfile = { ...profileDraft, name }; setProfileSaving(true); try { const savedProfile = await persistProfile(nextProfile); setProfile(savedProfile); setProfileDraft(savedProfile); setStartGreeting(createStartGreeting(savedProfile.name)); setDraft(""); setPresetMessageLength(0); const nextStep = missingProfileStep(savedProfile); if (nextStep) setOnboardingStep(nextStep); else setOnboardingJustCompleted(false); } catch (caught) { notifyError(friendlyError(caught instanceof Error ? caught.message : "称呼保存失败")); } finally { setProfileSaving(false); } } async function saveOnboardingBirth(event: FormEvent) { event.preventDefault(); if (!isBirthTimeDraftReady(profileDraft) || !account || profileSaving) return; setProfileSaving(true); setBirthTimeAssessmentPhase("saving_profile"); try { const savedProfile = await persistProfile(profileDraft); birthTimeRevisionPendingRef.current = false; setProfile(savedProfile); setProfileDraft(savedProfile); setPresetMessageLength(0); const nextStep = missingProfileStep(savedProfile); if (nextStep) setOnboardingStep(nextStep); else setOnboardingJustCompleted(false); } catch (caught) { notifyError(friendlyError(caught instanceof Error ? caught.message : "出生时间保存失败")); } finally { setBirthTimeAssessmentPhase(null); setProfileSaving(false); } } function editDeclaredBirthTimeDetails() { birthTimeRevisionPendingRef.current = true; setBirthTimeError(""); setPresetMessageLength(0); setOnboardingStep("birth"); } async function saveOnboardingPlace(event: FormEvent) { event.preventDefault(); if (!selectedBirthPlace(profileDraft) || !account || profileSaving) return; setProfileSaving(true); setBirthTimeAssessmentPhase("entering_home"); try { const savedProfile = await persistProfile(profileDraft); setProfile(savedProfile); setProfileDraft(savedProfile); setStartGreeting(createStartGreeting(savedProfile.name)); setPresetMessageLength(0); setOnboardingJustCompleted(false); // Fire and forget: the flow above has already moved on and never waits for it. onOwnBirthPlaceSaved?.(); } catch (caught) { notifyError(friendlyError(caught instanceof Error ? caught.message : "出生地点保存失败")); } finally { setBirthTimeAssessmentPhase(null); setProfileSaving(false); } } function completeGuidedBirthTime(result: JourneyClientResponse) { if (result.nextAction.kind !== "ready") return; const confirmedProfile: Profile = { ...profileDraft, time: result.nextAction.activeTime, birthTimeStatus: "confirmed", rectificationCaseId: result.caseId, }; setProfile(confirmedProfile); setProfileDraft(confirmedProfile); setPresetMessageLength(0); setOnboardingJustCompleted(false); } async function retryBirthTimeAssessment() { if (!account || profileSaving) return; setProfileSaving(true); setBirthTimeError(""); try { const assessedProfile = await assessSavedBirthTime(profileDraft); setPresetMessageLength(0); if (assessedProfile.birthTimeStatus === "confirmed") setOnboardingJustCompleted(false); } catch (caught) { setBirthTimeError(caught instanceof Error ? caught.message : "生时评估暂时不可用,请稍后重试。"); } finally { setProfileSaving(false); } } async function signOut() { if (signingOut) return; setSigningOut(true); try { // Signing out ends this account's warm return to `/` (BUG-1040); a // failed sign-out only costs one loading ring on the next return. clearHomeWarmSnapshot(); await selfHostedOtpActions.signOut(); window.location.assign("/login"); } catch (caught) { const message = caught instanceof Error ? caught.message : "退出失败"; notifyError(friendlyError(message)); setSigningOut(false); } } return { profile, setProfile, profileDraft, setProfileDraft, profileNotice, setProfileNotice, avatarSaving, profileSaving, refreshAccount, openAccountDialog, closeAccountDialog, persistAvatar, persistProfile, assessSavedBirthTime, saveOnboardingName, saveOnboardingBirth, editDeclaredBirthTimeDetails, saveOnboardingPlace, completeGuidedBirthTime, retryBirthTimeAssessment, signOut, }; }