Fix save progress bug
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
} from "./utils/createFlowScreenRegistry";
|
||||
import Button from "../../components/buttons/Button";
|
||||
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
|
||||
import { buildCreateFlowDraftPayload } from "../../../lib/create/buildCreateFlowDraftPayload";
|
||||
import {
|
||||
fetchAuthSession,
|
||||
requestMagicLink,
|
||||
@@ -52,6 +53,7 @@ import { safeInternalPath } from "../../../lib/safeInternalPath";
|
||||
import {
|
||||
clearAnonymousCreateFlowStorage,
|
||||
setTransferPendingFlag,
|
||||
writeAnonymousCreateFlowState,
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import {
|
||||
createFlowStateFromPublishedRule,
|
||||
@@ -396,7 +398,15 @@ function CreateFlowLayoutContent({
|
||||
const segment = stepAfterSave ?? "review";
|
||||
const rawNext = `/create/${segment}?syncDraft=1`;
|
||||
const nextPath = safeInternalPath(rawNext);
|
||||
const result = await requestMagicLink(trimmed, nextPath);
|
||||
const draftPayload = buildCreateFlowDraftPayload(state, currentStep);
|
||||
writeAnonymousCreateFlowState({
|
||||
...draftPayload,
|
||||
communitySaveEmail: trimmed,
|
||||
});
|
||||
const result = await requestMagicLink(trimmed, nextPath, {
|
||||
...draftPayload,
|
||||
communitySaveEmail: trimmed,
|
||||
});
|
||||
if (result.ok === false) {
|
||||
if (result.retryAfterMs != null && result.retryAfterMs > 0) {
|
||||
const seconds = Math.ceil(result.retryAfterMs / 1000);
|
||||
@@ -418,7 +428,7 @@ function CreateFlowLayoutContent({
|
||||
} finally {
|
||||
setCommunitySaveMagicLinkSubmitting(false);
|
||||
}
|
||||
}, [state.communitySaveEmail, tLogin, updateState]);
|
||||
}, [state, currentStep, tLogin, updateState]);
|
||||
|
||||
const isCompletedStep = currentStep === "completed";
|
||||
const isRightRailStep = currentStep === "decision-approaches";
|
||||
|
||||
@@ -9,16 +9,55 @@ import {
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import { useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
|
||||
import { saveDraftToServer } from "../../../lib/create/api";
|
||||
import { fetchDraftFromServer, saveDraftToServer } from "../../../lib/create/api";
|
||||
import { createFlowStateHasKeys } from "../../../lib/create/draftHydrationUtils";
|
||||
import type { CreateFlowState } from "./types";
|
||||
import messages from "../../../messages/en/index";
|
||||
import Alert from "../../components/modals/Alert";
|
||||
|
||||
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
|
||||
|
||||
function buildPayloadWithStep(
|
||||
base: CreateFlowState,
|
||||
pathname: string | null,
|
||||
): CreateFlowState {
|
||||
const step =
|
||||
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
|
||||
return {
|
||||
...base,
|
||||
...(step ? { currentStep: step } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer the on-device anonymous mirror when present; otherwise use the draft
|
||||
* stored on the magic-link token at request time (written during verify).
|
||||
*/
|
||||
async function resolvePostLoginDraftPayload(
|
||||
local: CreateFlowState,
|
||||
pathname: string | null,
|
||||
): Promise<CreateFlowState | null> {
|
||||
const localPayload = createFlowStateHasKeys(local)
|
||||
? buildPayloadWithStep(local, pathname)
|
||||
: null;
|
||||
|
||||
const serverDraft = await fetchDraftFromServer();
|
||||
const serverPayload =
|
||||
serverDraft != null && createFlowStateHasKeys(serverDraft)
|
||||
? buildPayloadWithStep(serverDraft, pathname)
|
||||
: null;
|
||||
|
||||
if (localPayload && serverPayload) {
|
||||
return { ...serverPayload, ...localPayload };
|
||||
}
|
||||
return localPayload ?? serverPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* After magic-link verify, redirects to `/create/...?syncDraft=1` with session cookie.
|
||||
* With backend sync: PUT draft once then hydrates context. Without sync: hydrates from
|
||||
* `create-flow-anonymous` localStorage only (no server write).
|
||||
* With backend sync: PUT draft once when the device mirror is non-empty, then hydrates
|
||||
* context. Without sync: hydrates from localStorage and/or the server draft saved at
|
||||
* verify. Never writes an empty payload over an existing server draft.
|
||||
*/
|
||||
export function PostLoginDraftTransfer({
|
||||
sessionUser,
|
||||
@@ -39,49 +78,6 @@ export function PostLoginDraftTransfer({
|
||||
if (!wantsTransfer) return;
|
||||
if (attemptedRef.current) return;
|
||||
|
||||
if (!SYNC_ENABLED) {
|
||||
attemptedRef.current = true;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const local = readAnonymousCreateFlowState();
|
||||
const pending = hasTransferPendingFlag();
|
||||
|
||||
if (Object.keys(local).length === 0 && !pending) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("syncDraft");
|
||||
const q = params.toString();
|
||||
if (pathname) {
|
||||
router.replace(q ? `${pathname}?${q}` : pathname);
|
||||
}
|
||||
attemptedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const step =
|
||||
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
|
||||
const payload = {
|
||||
...local,
|
||||
...(step ? { currentStep: step } : {}),
|
||||
};
|
||||
|
||||
if (cancelled) return;
|
||||
clearAnonymousCreateFlowStorage();
|
||||
replaceState(payload);
|
||||
|
||||
if (cancelled) return;
|
||||
if (pathname) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("syncDraft");
|
||||
const q = params.toString();
|
||||
router.replace(q ? `${pathname}?${q}` : pathname);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
attemptedRef.current = true;
|
||||
|
||||
let cancelled = false;
|
||||
@@ -90,7 +86,7 @@ export function PostLoginDraftTransfer({
|
||||
const local = readAnonymousCreateFlowState();
|
||||
const pending = hasTransferPendingFlag();
|
||||
|
||||
if (Object.keys(local).length === 0 && !pending) {
|
||||
if (!createFlowStateHasKeys(local) && !pending) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("syncDraft");
|
||||
const q = params.toString();
|
||||
@@ -101,27 +97,36 @@ export function PostLoginDraftTransfer({
|
||||
return;
|
||||
}
|
||||
|
||||
const step =
|
||||
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
|
||||
const payload = {
|
||||
...local,
|
||||
...(step ? { currentStep: step } : {}),
|
||||
};
|
||||
|
||||
const saveResult = await saveDraftToServer(payload);
|
||||
const payload = await resolvePostLoginDraftPayload(local, pathname);
|
||||
if (cancelled) return;
|
||||
|
||||
if (saveResult.ok === false) {
|
||||
setTransferError(
|
||||
messages.create.topNav.postLoginSaveFailedWithReason.replace(
|
||||
"{reason}",
|
||||
saveResult.message,
|
||||
),
|
||||
);
|
||||
if (payload == null || !createFlowStateHasKeys(payload)) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("syncDraft");
|
||||
const q = params.toString();
|
||||
if (pathname) {
|
||||
router.replace(q ? `${pathname}?${q}` : pathname);
|
||||
}
|
||||
attemptedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (SYNC_ENABLED && createFlowStateHasKeys(local)) {
|
||||
const saveResult = await saveDraftToServer(payload);
|
||||
if (cancelled) return;
|
||||
|
||||
if (saveResult.ok === false) {
|
||||
setTransferError(
|
||||
messages.create.topNav.postLoginSaveFailedWithReason.replace(
|
||||
"{reason}",
|
||||
saveResult.message,
|
||||
),
|
||||
);
|
||||
attemptedRef.current = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
clearAnonymousCreateFlowStorage();
|
||||
replaceState(payload);
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ export function SignedInDraftHydration({
|
||||
if (finishedUserIdRef.current === userId) return;
|
||||
|
||||
if (syncDraftParam === "1" || hasTransferPendingFlag()) {
|
||||
finishedUserIdRef.current = userId;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
import {
|
||||
clearAnonymousCreateFlowStorage,
|
||||
clearLegacyCreateFlowKeysOnce,
|
||||
hasTransferPendingFlag,
|
||||
readAnonymousCreateFlowState,
|
||||
writeAnonymousCreateFlowState,
|
||||
} from "../utils/anonymousDraftStorage";
|
||||
@@ -94,6 +95,13 @@ export function CreateFlowProvider({
|
||||
const wasOff = !prevPersistRef.current;
|
||||
prevPersistRef.current = true;
|
||||
if (!wasOff) return;
|
||||
if (hasTransferPendingFlag()) return;
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
new URLSearchParams(window.location.search).get("syncDraft") === "1"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const from = readAnonymousCreateFlowState();
|
||||
if (Object.keys(from).length === 0) return;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydrate local draft when mirroring turns on
|
||||
|
||||
Reference in New Issue
Block a user