From 8405c06c5fa728a144fe0feee79dacaa16636240 Mon Sep 17 00:00:00 2001 From: Malcolm-Wander Date: Tue, 25 Aug 2026 17:49:23 +0100 Subject: [PATCH] Updated project files Changes made --- apps/web/lib/__tests__/huntStore.test.ts | 20 +++ apps/web/lib/context/WalletContext.tsx | 3 + apps/web/lib/huntStore.ts | 206 +++++++++++++++++++++-- 3 files changed, 214 insertions(+), 15 deletions(-) diff --git a/apps/web/lib/__tests__/huntStore.test.ts b/apps/web/lib/__tests__/huntStore.test.ts index f0ca1f1f9..f88e534c6 100644 --- a/apps/web/lib/__tests__/huntStore.test.ts +++ b/apps/web/lib/__tests__/huntStore.test.ts @@ -34,6 +34,8 @@ import { advanceHuntProgress, clearHuntProgress, gcHunt, + migrateGuestProgressToWallet, + getWalletProgressKey, MAX_CLUES_PER_HUNT, takeHuntStoreSnapshot, updateClueAnswer, @@ -1260,6 +1262,24 @@ describe("huntStore", () => { clearHuntProgress(9777); expect(localStorage.getItem("hunty_hunt_progress_9777")).toBeNull(); }); + + it("migrates guest progress into a wallet-scoped key and stays idempotent", () => { + const guestProgress = startHuntProgress(8765); + const walletAddress = "GABC1234567890"; + + const migrated = migrateGuestProgressToWallet(8765, walletAddress); + + expect(migrated).toEqual({ + ...guestProgress, + currentClueIndex: guestProgress.currentClueIndex, + }); + expect(localStorage.getItem(getWalletProgressKey(8765, walletAddress))).toBeTruthy(); + expect(localStorage.getItem("hunty_hunt_progress_8765")).toBeNull(); + + const duplicate = migrateGuestProgressToWallet(8765, walletAddress); + expect(duplicate).toEqual(migrated); + expect(localStorage.getItem(getWalletProgressKey(8765, walletAddress))).toBeTruthy(); + }); }); describe("gcHunt", () => { diff --git a/apps/web/lib/context/WalletContext.tsx b/apps/web/lib/context/WalletContext.tsx index 1eb086df9..384ab73ee 100644 --- a/apps/web/lib/context/WalletContext.tsx +++ b/apps/web/lib/context/WalletContext.tsx @@ -17,6 +17,7 @@ import { } from "react" import { useIsMounted } from "@/hooks/useIsMounted" +import { migrateGuestProgressToWallet } from "@/lib/huntStore" import { clearStoredWalletSession, connectWalletProvider, @@ -195,6 +196,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { setStoredWalletSession("freighter", address) localStorage.setItem(STORAGE_KEY, address) + migrateGuestProgressToWallet(address) setPublicKey(address) setWalletProvider("freighter") setConnected(true) @@ -205,6 +207,7 @@ export function WalletProvider({ children }: { children: ReactNode }) { const address = await connectWalletProvider(provider) setStoredWalletSession(provider, address) localStorage.setItem(STORAGE_KEY, address) + migrateGuestProgressToWallet(address) setPublicKey(address) setWalletProvider(provider) setConnected(true) diff --git a/apps/web/lib/huntStore.ts b/apps/web/lib/huntStore.ts index 348196ed6..9429b851d 100644 --- a/apps/web/lib/huntStore.ts +++ b/apps/web/lib/huntStore.ts @@ -207,21 +207,90 @@ function getProgressKey(huntId: number): string { return `${HUNT_PROGRESS_KEY_PREFIX}${huntId}`; } -function readProgressEntry(huntId: number): HuntProgressSnapshot | null { +export function getWalletProgressKey( + huntId: number, + walletAddress: string, +): string { + if (!walletAddress?.trim()) { + return getProgressKey(huntId); + } + + const scope = walletAddress.trim().toLowerCase(); + return `hunty_hunt_progress_wallet_${scope}_${huntId}`; +} + +function getWalletAddressFromStorage(): string | null { + if (typeof window === "undefined") return null; + + const directKeys = [ + "freighter_public_key", + "wallet_public_key", + "hunty_wallet_public_key", + ]; + + for (const key of directKeys) { + const value = localStorage.getItem(key)?.trim(); + if (value) return value; + } + + const walletStoreRaw = localStorage.getItem("hunty_wallet_store"); + if (!walletStoreRaw) return null; + + try { + const parsed = JSON.parse(walletStoreRaw) as { + state?: { publicKey?: string }; + publicKey?: string; + }; + const value = parsed?.state?.publicKey ?? parsed?.publicKey; + if (typeof value === "string" && value.trim()) return value.trim(); + } catch { + // ignore malformed wallet-store payloads and fall back to legacy keys + } + + return null; +} + +function resolveProgressStorageKey(huntId: number, walletAddress?: string | null) { + const activeWalletAddress = walletAddress?.trim() || getWalletAddressFromStorage(); + return activeWalletAddress + ? getWalletProgressKey(huntId, activeWalletAddress) + : getProgressKey(huntId); +} + +function readProgressEntry( + huntId: number, + walletAddress?: string | null, +): HuntProgressSnapshot | null { if (typeof window === "undefined") return null; + + const activeWalletAddress = walletAddress?.trim() || getWalletAddressFromStorage(); + const walletKey = activeWalletAddress + ? getWalletProgressKey(huntId, activeWalletAddress) + : null; + try { - const raw = localStorage.getItem(getProgressKey(huntId)); - return raw ? (JSON.parse(raw) as HuntProgressSnapshot) : null; + if (walletKey) { + const walletRaw = localStorage.getItem(walletKey); + if (walletRaw) { + return JSON.parse(walletRaw) as HuntProgressSnapshot; + } + } + + const legacyRaw = localStorage.getItem(getProgressKey(huntId)); + return legacyRaw ? (JSON.parse(legacyRaw) as HuntProgressSnapshot) : null; } catch { return null; } } -function writeProgressEntry(progress: HuntProgressSnapshot): void { +function writeProgressEntry( + progress: HuntProgressSnapshot, + walletAddress?: string | null, +): void { if (typeof window === "undefined") return; try { localStorage.setItem( - getProgressKey(progress.huntId), + resolveProgressStorageKey(progress.huntId, walletAddress), JSON.stringify(progress), ); } catch { @@ -229,6 +298,106 @@ function writeProgressEntry(progress: HuntProgressSnapshot): void { } } +function mergeProgressSnapshots( + current: HuntProgressSnapshot, + incoming: HuntProgressSnapshot, +): HuntProgressSnapshot { + const currentClueIndex = Math.max( + current.currentClueIndex, + incoming.currentClueIndex, + ); + const startedAt = Math.min(current.startedAt || incoming.startedAt, incoming.startedAt || current.startedAt || Date.now()); + const completed = current.completed || incoming.completed; + const completedAt = + current.completedAt && incoming.completedAt + ? new Date( + Math.max(new Date(current.completedAt).getTime(), new Date(incoming.completedAt).getTime()), + ).getTime() + : current.completedAt ?? incoming.completedAt; + + return { + ...current, + ...incoming, + huntId: current.huntId || incoming.huntId, + currentClueIndex, + startedAt, + completed, + completedAt, + }; +} + +export function migrateGuestProgressToWallet( + walletAddressOrHuntId: string | number, + walletAddressOrHuntIdValue?: string | number | null, +): HuntProgressSnapshot | null { + if (typeof window === "undefined") return null; + + const isWalletArgumentFirst = typeof walletAddressOrHuntId === "string"; + const walletAddress = (isWalletArgumentFirst + ? walletAddressOrHuntId + : String(walletAddressOrHuntIdValue ?? "")) + .trim(); + const huntId = isWalletArgumentFirst + ? Number(walletAddressOrHuntIdValue ?? NaN) + : Number(walletAddressOrHuntId); + + if (!walletAddress) { + return null; + } + + const hasSpecificHunt = Number.isFinite(huntId); + const guestKeys = hasSpecificHunt + ? [getProgressKey(huntId)] + : Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index)) + .filter( + (key): key is string => + typeof key === "string" && key.startsWith(HUNT_PROGRESS_KEY_PREFIX), + ) + .filter((key) => !key.includes("wallet_")); + + let lastMigrated: HuntProgressSnapshot | null = null; + + for (const guestKey of guestKeys) { + const guestHuntId = Number(guestKey.replace(HUNT_PROGRESS_KEY_PREFIX, "")); + if (!Number.isFinite(guestHuntId)) continue; + + const guestProgress = (() => { + try { + const raw = localStorage.getItem(guestKey); + return raw ? (JSON.parse(raw) as HuntProgressSnapshot) : null; + } catch { + return null; + } + })(); + + if (!guestProgress) continue; + + const walletKey = getWalletProgressKey(guestHuntId, walletAddress); + const storedWalletProgress = (() => { + try { + const raw = localStorage.getItem(walletKey); + return raw ? (JSON.parse(raw) as HuntProgressSnapshot) : null; + } catch { + return null; + } + })(); + + const mergedProgress = storedWalletProgress + ? mergeProgressSnapshots(storedWalletProgress, guestProgress) + : guestProgress; + + localStorage.setItem(walletKey, JSON.stringify(mergedProgress)); + localStorage.removeItem(guestKey); + lastMigrated = mergedProgress; + + if (hasSpecificHunt) { + return mergedProgress; + } + } + + return lastMigrated; +} + function measureStorageEntrySize(key: string, value: string): number { return new TextEncoder().encode(`${key}:${value}`).length; } @@ -734,8 +903,11 @@ export function updateClueAnswer( } /** Reads the current per-hunt progress snapshot. */ -export function getHuntProgress(huntId: number): HuntProgressSnapshot { - const existing = readProgressEntry(huntId); +export function getHuntProgress( + huntId: number, + walletAddress?: string | null, +): HuntProgressSnapshot { + const existing = readProgressEntry(huntId, walletAddress); if (existing) { return existing; } @@ -746,18 +918,21 @@ export function getHuntProgress(huntId: number): HuntProgressSnapshot { startedAt: Date.now(), completed: false, }; - writeProgressEntry(initial); + writeProgressEntry(initial, walletAddress); return initial; } /** Records that a hunt has started for the current browser session. */ -export function startHuntProgress(huntId: number): HuntProgressSnapshot { - const current = getHuntProgress(huntId); +export function startHuntProgress( + huntId: number, + walletAddress?: string | null, +): HuntProgressSnapshot { + const current = getHuntProgress(huntId, walletAddress); const next: HuntProgressSnapshot = { ...current, startedAt: current.startedAt || Date.now(), }; - writeProgressEntry(next); + writeProgressEntry(next, walletAddress); return next; } @@ -766,8 +941,9 @@ export function advanceHuntProgress( huntId: number, nextClueIndex: number, totalClues: number, + walletAddress?: string | null, ): HuntProgressSnapshot { - const current = getHuntProgress(huntId); + const current = getHuntProgress(huntId, walletAddress); const completed = nextClueIndex >= totalClues; const next: HuntProgressSnapshot = { ...current, @@ -775,14 +951,14 @@ export function advanceHuntProgress( completed, completedAt: completed ? Date.now() : current.completedAt, }; - writeProgressEntry(next); + writeProgressEntry(next, walletAddress); return next; } /** Clears the tracked hunt progress for the current browser session. */ -export function clearHuntProgress(huntId: number): void { +export function clearHuntProgress(huntId: number, walletAddress?: string | null): void { if (typeof window === "undefined") return; - localStorage.removeItem(getProgressKey(huntId)); + localStorage.removeItem(resolveProgressStorageKey(huntId, walletAddress)); } /**