From a0ea7a3e60a2c5780c1a3bfd3335a237e5c69441 Mon Sep 17 00:00:00 2001 From: icc Date: Sun, 31 May 2026 01:37:17 +0800 Subject: [PATCH] fix: allow undoing discarded policy drafts --- .../app/create/create-page-client.test.tsx | 56 +++++++++++++++++-- .../src/app/create/create-page-client.tsx | 32 +++++------ frontend/src/hooks/use-autosave.ts | 12 +++- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/create/create-page-client.test.tsx b/frontend/src/app/create/create-page-client.test.tsx index 45eeb0f..0e0b6d8 100644 --- a/frontend/src/app/create/create-page-client.test.tsx +++ b/frontend/src/app/create/create-page-client.test.tsx @@ -2,6 +2,8 @@ import React from "react"; import { act, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { LanguageProvider } from "@/i18n/provider"; + import CreatePolicyPageClient from "./create-page-client"; vi.mock("@/components/wallet-provider", () => ({ @@ -61,9 +63,17 @@ vi.mock("@/components/oracle-source-selector", () => ({ ) : (

Loading oracle providers...

- ), + ), })); +function renderCreatePolicyPageClient() { + return render( + + + , + ); +} + describe("CreatePolicyPageClient", () => { beforeEach(() => { vi.useFakeTimers(); @@ -77,14 +87,14 @@ describe("CreatePolicyPageClient", () => { }); it("moves from policy type selection into configure step", () => { - render(); + renderCreatePolicyPageClient(); fireEvent.click(screen.getByRole("radio", { name: /weather protection/i })); expect(screen.getByRole("heading", { name: /configure your policy/i })).toBeInTheDocument(); }); it("applies form validation for invalid coverage values", () => { - render(); + renderCreatePolicyPageClient(); fireEvent.click(screen.getByRole("radio", { name: /weather protection/i })); const coverageInput = screen.getByLabelText(/coverage amount/i); @@ -110,7 +120,7 @@ describe("CreatePolicyPageClient", () => { updatedAt: Date.now(), }), ); - render(); + renderCreatePolicyPageClient(); expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument(); @@ -144,7 +154,7 @@ describe("CreatePolicyPageClient", () => { updatedAt: Date.now(), }), ); - render(); + renderCreatePolicyPageClient(); expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument(); @@ -155,4 +165,40 @@ describe("CreatePolicyPageClient", () => { const triggerInput = screen.getByPlaceholderText("Trigger mock input") as HTMLInputElement; expect(triggerInput.value).toBe("temperature > 25 AND rainfall > 50"); }); + + it("lets users undo an explicit draft discard", () => { + const savedDraft = { + policyType: "weather", + coverageAmount: "5000", + premium: "120", + triggerCondition: "rainfall > 50", + duration: "90", + oracleProvider: "weatherlink-prime", + }; + localStorage.setItem( + "stellarinsure-policy-draft", + JSON.stringify({ data: savedDraft, updatedAt: 1000 }), + ); + + renderCreatePolicyPageClient(); + + expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /discard/i })); + + expect(screen.getByText(/draft discarded/i)).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: /choose a policy type/i })).toBeInTheDocument(); + expect(localStorage.getItem("stellarinsure-policy-draft")).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: /undo discard/i })); + + expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument(); + expect(screen.getByText(/rainfall > 50/i)).toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(600); + }); + + expect(JSON.parse(localStorage.getItem("stellarinsure-policy-draft") ?? "{}").data).toEqual(savedDraft); + }); }); diff --git a/frontend/src/app/create/create-page-client.tsx b/frontend/src/app/create/create-page-client.tsx index b140a7a..f18a945 100644 --- a/frontend/src/app/create/create-page-client.tsx +++ b/frontend/src/app/create/create-page-client.tsx @@ -56,7 +56,11 @@ const INITIAL_DRAFT: PolicyDraft = { oracleProvider: "", }; - +function getDraftStep(draft: PolicyDraft): CreateStep { + if (draft.policyType && draft.coverageAmount && draft.triggerCondition) return 2; + if (draft.policyType) return 1; + return 0; +} function parseConditionString(condition: string): ConditionRule[] | undefined { if (!condition || condition.trim() === "") return undefined; @@ -340,11 +344,7 @@ export default function CreatePolicyPageClient() { hasClearedBackup, isSaved, } = usePolicyDraftAutosave("stellarinsure-policy-draft", INITIAL_DRAFT); - const [step, setStep] = useState(() => { - if (draft.policyType && draft.coverageAmount && draft.triggerCondition) return 2; - if (draft.policyType) return 1; - return 0; - }); + const [step, setStep] = useState(() => getDraftStep(draft)); const [txSteps, setTxSteps] = useState(DEFAULT_TX_STEPS); const [coverageTouched, setCoverageTouched] = useState(false); const [premiumTouched, setPremiumTouched] = useState(false); @@ -369,6 +369,13 @@ export default function CreatePolicyPageClient() { setReceipt(null); } + function handleDiscardDraft() { + clearDraft(); + setStep(0); + setReceipt(null); + setValidationErrors([]); + } + useEffect(() => { const selectedPolicyType = draft.policyType; @@ -576,13 +583,7 @@ export default function CreatePolicyPageClient() { if (!restored) { return; } - if (restored.policyType && restored.coverageAmount && restored.triggerCondition) { - setStep(2); - } else if (restored.policyType) { - setStep(1); - } else { - setStep(0); - } + setStep(getDraftStep(restored)); }} > Undo discard @@ -800,10 +801,7 @@ export default function CreatePolicyPageClient() { diff --git a/frontend/src/hooks/use-autosave.ts b/frontend/src/hooks/use-autosave.ts index 03639f9..0674a50 100644 --- a/frontend/src/hooks/use-autosave.ts +++ b/frontend/src/hooks/use-autosave.ts @@ -53,6 +53,7 @@ export function useAutosave(key: string, initial: T): [T, (next: T) => void, export function usePolicyDraftAutosave(key: string, initial: T): AutosaveControls { const backupKey = `${key}-backup`; const editedAtRef = useRef(0); + const clearedAtRef = useRef(null); const [state, setStateInternal] = useState(() => { const stored = readStoredDraft(key); @@ -139,6 +140,7 @@ export function usePolicyDraftAutosave(key: string, initial: T): AutosaveCont }, [key]); const clear = useCallback(() => { + const clearedAt = Date.now(); try { const current = readStoredDraft(key); if (current) { @@ -149,7 +151,8 @@ export function usePolicyDraftAutosave(key: string, initial: T): AutosaveCont // Ignore backup failures. } - editedAtRef.current = Date.now(); + clearedAtRef.current = clearedAt; + editedAtRef.current = clearedAt; setStateInternal(initial); setIsSaved(true); @@ -174,11 +177,16 @@ export function usePolicyDraftAutosave(key: string, initial: T): AutosaveCont const backup = JSON.parse(backupRaw) as StoredDraft; const restoredAt = backup.updatedAt ?? Date.now(); - if (restoredAt <= editedAtRef.current) { + if (clearedAtRef.current === null && restoredAt <= editedAtRef.current) { + return null; + } + + if (clearedAtRef.current !== null && editedAtRef.current > clearedAtRef.current) { return null; } editedAtRef.current = restoredAt; + clearedAtRef.current = null; setStateInternal(backup.data); writeStoredDraft(key, backup.data, restoredAt); localStorage.removeItem(backupKey);