fix: persist saved client delivery choice

This commit is contained in:
Codex 2026-04-16 18:04:45 +03:00
parent de0cf49490
commit 5142ff30db
3 changed files with 97 additions and 3 deletions

View File

@ -143,6 +143,9 @@ export const ClientDeliveryPage = () => {
const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot; const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot;
const isChoiceSaved = choiceSaved || (!isActiveState && Boolean(invitationSelectedSlot)); const isChoiceSaved = choiceSaved || (!isActiveState && Boolean(invitationSelectedSlot));
const savedChoiceLabel = effectiveSelectedSlot
? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}`
: "";
const handleSaveChoice = React.useCallback( const handleSaveChoice = React.useCallback(
async () => { async () => {
@ -236,6 +239,16 @@ export const ClientDeliveryPage = () => {
</p> </p>
</Panel> </Panel>
{isChoiceSaved && savedChoiceLabel ? (
<Panel className="space-y-2 p-5 sm:p-6">
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Ваш выбор</p>
<h2 className="text-xl font-semibold leading-tight">Сохранено: {savedChoiceLabel}</h2>
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
При повторном открытии этой ссылки будет показан тот же выбор.
</p>
</Panel>
) : null}
{isActiveState && !isChoiceSaved && slots.length ? ( {isActiveState && !isChoiceSaved && slots.length ? (
<DeliverySlotsPicker <DeliverySlotsPicker
slots={slots} slots={slots}

View File

@ -3,6 +3,7 @@ import { supabase, hasSupabaseConfig } from "../supabaseClient";
const SHOWCASE_TOKEN = "showcase"; const SHOWCASE_TOKEN = "showcase";
const LOCAL_CLIENT_FLOW_TOKEN_PREFIX = "client-flow-"; const LOCAL_CLIENT_FLOW_TOKEN_PREFIX = "client-flow-";
const LOCAL_CLIENT_FLOW_TOKEN = "client-flow-1001"; const LOCAL_CLIENT_FLOW_TOKEN = "client-flow-1001";
const LOCAL_INVITATION_STORAGE_PREFIX = "delivery-invitation:";
const localDeliveryInvitationCache = new Map(); const localDeliveryInvitationCache = new Map();
@ -16,6 +17,43 @@ const addDays = (date, days) => {
const cloneInvitation = (invitation) => JSON.parse(JSON.stringify(invitation)); const cloneInvitation = (invitation) => JSON.parse(JSON.stringify(invitation));
const getLocalStorage = () => {
try {
return globalThis.localStorage || null;
} catch (error) {
return null;
}
};
const getLocalStorageKey = (token) => `${LOCAL_INVITATION_STORAGE_PREFIX}${token}`;
const readStoredInvitation = (token) => {
const storage = getLocalStorage();
if (!storage) {
return null;
}
try {
const raw = storage.getItem(getLocalStorageKey(token));
return raw ? JSON.parse(raw) : null;
} catch (error) {
return null;
}
};
const writeStoredInvitation = (invitation) => {
const storage = getLocalStorage();
if (!storage?.setItem) {
return;
}
try {
storage.setItem(getLocalStorageKey(invitation.token), JSON.stringify(invitation));
} catch (error) {
// Ignore storage quota and privacy mode failures.
}
};
const isLocalClientInvitationToken = (token) => const isLocalClientInvitationToken = (token) =>
token === SHOWCASE_TOKEN || token?.startsWith(LOCAL_CLIENT_FLOW_TOKEN_PREFIX); token === SHOWCASE_TOKEN || token?.startsWith(LOCAL_CLIENT_FLOW_TOKEN_PREFIX);
@ -52,8 +90,14 @@ const getCachedInvitation = (token) => {
}; };
const cacheInvitation = (invitation) => { const cacheInvitation = (invitation) => {
localDeliveryInvitationCache.set(invitation.token, cloneInvitation(invitation)); const clonedInvitation = cloneInvitation(invitation);
return getCachedInvitation(invitation.token); localDeliveryInvitationCache.set(clonedInvitation.token, clonedInvitation);
if (isLocalClientInvitationToken(clonedInvitation.token)) {
writeStoredInvitation(clonedInvitation);
}
return getCachedInvitation(clonedInvitation.token);
}; };
const buildFallbackInvitation = (token) => const buildFallbackInvitation = (token) =>
@ -125,7 +169,10 @@ export const fetchDeliveryInvitation = async (token) => {
if (isLocalClientInvitationToken(token)) { if (isLocalClientInvitationToken(token)) {
const cachedInvitation = getCachedInvitation(token); const cachedInvitation = getCachedInvitation(token);
const invitation = cachedInvitation ?? cacheInvitation(buildFallbackInvitation(token)); const storedInvitation = cachedInvitation ?? readStoredInvitation(token);
const invitation = storedInvitation
? cacheInvitation(storedInvitation)
: cacheInvitation(buildFallbackInvitation(token));
warmLocalInvitationCache(token, "get-delivery-invitation"); warmLocalInvitationCache(token, "get-delivery-invitation");
return invitation; return invitation;
} }

View File

@ -27,6 +27,19 @@ describe("deliveryInvitationApi", () => {
beforeEach(() => { beforeEach(() => {
invoke.mockReset(); invoke.mockReset();
__resetLocalDeliveryInvitationCache(); __resetLocalDeliveryInvitationCache();
const storage = new Map();
vi.stubGlobal("localStorage", {
getItem: (key) => storage.get(key) ?? null,
setItem: (key, value) => {
storage.set(key, String(value));
},
removeItem: (key) => {
storage.delete(key);
},
clear: () => {
storage.clear();
},
});
}); });
it("loads a delivery invitation by token", async () => { it("loads a delivery invitation by token", async () => {
@ -173,6 +186,27 @@ describe("deliveryInvitationApi", () => {
}); });
}); });
it("restores the saved local client invitation after cache reset", async () => {
invoke.mockRejectedValueOnce(new Error("worker boot error"));
await fetchDeliveryInvitation("client-flow-1001");
invoke.mockRejectedValueOnce(new Error("worker boot error"));
await confirmDeliveryChoice({
token: "client-flow-1001",
deliveryDate: "2026-04-16",
deliveryTime: "После обеда",
});
__resetLocalDeliveryInvitationCache();
await expect(fetchDeliveryInvitation("client-flow-1001")).resolves.toMatchObject({
token: "client-flow-1001",
deliveryDate: "2026-04-16",
deliveryTime: "После обеда",
state: "confirmed",
});
});
it("creates a delivery invitation from order data", async () => { it("creates a delivery invitation from order data", async () => {
invoke.mockResolvedValueOnce({ invoke.mockResolvedValueOnce({
data: { data: {