From a139516d7fba01d9f5423140265b193283f983d8 Mon Sep 17 00:00:00 2001 From: Mayur Shinde Date: Wed, 10 Jun 2026 13:26:10 +0530 Subject: [PATCH] remove localstorage dependency for wizard --- src/data/mockStore.jsx | 60 ++++++++++++------- .../estimateWizard/EstimateWizardContext.jsx | 44 +++----------- src/modules/estimateWizard/api/crmHandoff.js | 20 +++---- .../estimateWizard/data/tenantConfig.js | 23 ++++--- 4 files changed, 67 insertions(+), 80 deletions(-) diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 095922b..d26b2e8 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -9142,27 +9142,21 @@ const MockStoreContext = createContext(); // --------------------------------------------------------------------------- // Estimate Wizard → CRM lead persistence. -// The public DIY Estimate Wizard runs in a separate session from the authed CRM, -// so its leads must survive a reload. They're persisted to localStorage and -// rehydrated into the leads list on store init, deduped by wizard session id. +// The public DIY Estimate Wizard adds completed estimates as real leads through the +// in-memory store (addWizardLead → setLeads). Nothing is persisted to localStorage, +// so leads reset on a full page reload, like all CRM demo data. +// +// When the wizard runs in the SAME tab as the CRM it shares this provider, so the +// lead appears immediately. When it runs in a SEPARATE tab (the public /estimate +// link is its own JS context), we bridge it with BroadcastChannel — an in-memory, +// no-persistence message bus — so the lead still reaches the CRM tab live without +// writing anything to storage. // --------------------------------------------------------------------------- -const WIZARD_LEADS_KEY = 'lup_estimate_wizard_leads_v1'; +const WIZARD_CHANNEL = 'lup-estimate-wizard'; +const wizardBus = (typeof BroadcastChannel !== 'undefined') ? new BroadcastChannel(WIZARD_CHANNEL) : null; -function loadWizardLeads() { - try { - const raw = localStorage.getItem(WIZARD_LEADS_KEY); - const list = raw ? JSON.parse(raw) : []; - return Array.isArray(list) ? list : []; - } catch { return []; } -} - -function persistWizardLead(lead) { - try { - const list = loadWizardLeads(); - const sid = lead?.estimateWizard?.sessionId; - if (sid && list.some(l => l?.estimateWizard?.sessionId === sid)) return; // already saved - localStorage.setItem(WIZARD_LEADS_KEY, JSON.stringify([lead, ...list])); - } catch { /* quota */ } +function broadcastWizardLead(lead) { + try { wizardBus?.postMessage({ type: 'wizard-lead', lead }); } catch { /* unsupported */ } } export const MockStoreProvider = ({ children }) => { @@ -9179,7 +9173,7 @@ export const MockStoreProvider = ({ children }) => { const [projects, setProjects] = useState(MOCK_PROJECTS); const [orders, setOrders] = useState(MOCK_ORDERS); const [vendorInvoices, setVendorInvoices] = useState(MOCK_VENDOR_INVOICES); - const [leads, setLeads] = useState(() => [...loadWizardLeads(), ...seedLeadsFromAttribution()]); + const [leads, setLeads] = useState(() => seedLeadsFromAttribution()); const [dispatchLeads, setDispatchLeads] = useState(DISPATCH_LEADS_INITIAL); const [templates, setTemplates] = useState(MASTER_TEMPLATES_INITIAL); const [estimates, setEstimates] = useState(MOCK_ESTIMATES_INITIAL); @@ -9383,6 +9377,25 @@ export const MockStoreProvider = ({ children }) => { setProperties(data); }, []); + // Receive wizard leads created in another tab (public /estimate) and merge them + // into the leads list live — no reload, no localStorage. Deduped by wizard + // session id (and id) so an echo or repeat message can't double-add. + useEffect(() => { + if (typeof BroadcastChannel === 'undefined') return; + const bus = new BroadcastChannel(WIZARD_CHANNEL); + bus.onmessage = (e) => { + const lead = e?.data?.type === 'wizard-lead' ? e.data.lead : null; + if (!lead) return; + setLeads(prev => { + const sid = lead.estimateWizard?.sessionId; + if (sid && prev.some(l => l?.estimateWizard?.sessionId === sid)) return prev; + if (lead.id && prev.some(l => l?.id === lead.id)) return prev; + return [lead, ...prev]; + }); + }; + return () => bus.close(); + }, []); + const updatePropertyStatus = (id, newStatus) => { setProperties(prev => prev.map(p => p.id === id ? { ...p, canvassingStatus: newStatus } : p @@ -10004,8 +10017,9 @@ export const MockStoreProvider = ({ children }) => { return newLead; }, - // Lead created by the public Estimate Wizard. Persisted to localStorage - // (survives reload) and deduped by wizard session id. + // Lead created by the public Estimate Wizard. In-memory only (no + // localStorage) and deduped by wizard session id so a StrictMode double + // mount can't create the lead twice. addWizardLead: (lead) => { const created = { ...lead, @@ -10015,7 +10029,7 @@ export const MockStoreProvider = ({ children }) => { }; const sid = created.estimateWizard?.sessionId; setLeads(prev => (sid && prev.some(l => l?.estimateWizard?.sessionId === sid)) ? prev : [created, ...prev]); - persistWizardLead(created); + broadcastWizardLead(created); // reach the CRM tab if the wizard is in another tab return created; }, diff --git a/src/modules/estimateWizard/EstimateWizardContext.jsx b/src/modules/estimateWizard/EstimateWizardContext.jsx index 814aeeb..947c84a 100644 --- a/src/modules/estimateWizard/EstimateWizardContext.jsx +++ b/src/modules/estimateWizard/EstimateWizardContext.jsx @@ -4,8 +4,8 @@ * This is the client-side stand-in for the spec's backend entities (§12): * estimate_sessions, estimate_inputs, hail_risk_snapshots, recommendations, * customer_reports, estimate_events. - * For the demo it persists to localStorage so an abandoned session can be resumed - * (spec §13: "auto-save every screen to recover abandoned sessions"). + * Session state is held in React memory only — like the rest of the CRM demo store, + * nothing is written to localStorage, so a full page reload starts a fresh session. * * Kept separate from the global MockStoreProvider on purpose — this is a new, * isolated module. A Phase 5 adapter can mirror sessions into the CRM mock store. @@ -15,8 +15,6 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr import { STEP_FLOW } from './data/questions'; import { getTenantConfig, CONFIG_CHANGE_EVENT } from './data/tenantConfig'; -const STORAGE_KEY = 'lup_estimate_wizard_session_v1'; - // One-click demo seed (spec demo support). Every value matches a valid choice in // questions.js so the wizard renders selected cards correctly. Address is a real // Plano, TX location so the Nominatim geocoder + storm-history lookup resolve. @@ -80,43 +78,21 @@ function freshSession(source = 'public') { }; } -function loadSession() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return null; - const parsed = JSON.parse(raw); - // Basic shape guard so a stale/partial blob can't crash the wizard. - if (parsed && parsed.id && parsed.inputs) return parsed; - } catch { /* ignore corrupt storage */ } - return null; -} - export function EstimateWizardProvider({ children, source = 'public' }) { // Snapshot tenant config at mount, but stay reactive: the admin can publish flags // (kill-switch, AI copy, conservative mode), pricing, and package content while a - // wizard is open. Refresh on same-tab publish, cross-tab storage writes, and on - // tab focus so toggles take effect without a hard reload (spec §14/§18). + // wizard is open. Refresh on the same-tab publish event so toggles take effect + // without a hard reload (spec §14/§18). const [tenantConfig, setTenantConfig] = useState(getTenantConfig); - const [session, setSession] = useState(() => loadSession() || freshSession(source)); - const [resumed] = useState(() => !!loadSession()); + const [session, setSession] = useState(() => freshSession(source)); + const [resumed] = useState(false); // no persistence → never a resumed session useEffect(() => { const refresh = () => setTenantConfig(getTenantConfig()); - window.addEventListener(CONFIG_CHANGE_EVENT, refresh); // same-tab admin publish - window.addEventListener('storage', refresh); // another tab published - window.addEventListener('focus', refresh); // returning to this tab - return () => { - window.removeEventListener(CONFIG_CHANGE_EVENT, refresh); - window.removeEventListener('storage', refresh); - window.removeEventListener('focus', refresh); - }; + window.addEventListener(CONFIG_CHANGE_EVENT, refresh); // admin publish + return () => window.removeEventListener(CONFIG_CHANGE_EVENT, refresh); }, []); - // Autosave on every change (debounced via microtask coalescing not needed at this scale). - useEffect(() => { - try { localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); } catch { /* quota */ } - }, [session]); - const logEvent = useCallback((eventType, payload = {}) => { setSession(s => ({ ...s, @@ -158,9 +134,7 @@ export function EstimateWizardProvider({ children, source = 'public' }) { }, []); const reset = useCallback(() => { - const fresh = freshSession(source); - setSession(fresh); - try { localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh)); } catch { /* quota */ } + setSession(freshSession(source)); }, [source]); // Fill every input with a valid demo answer so the wizard can be exercised fast. diff --git a/src/modules/estimateWizard/api/crmHandoff.js b/src/modules/estimateWizard/api/crmHandoff.js index 980d748..538ccb1 100644 --- a/src/modules/estimateWizard/api/crmHandoff.js +++ b/src/modules/estimateWizard/api/crmHandoff.js @@ -1,13 +1,16 @@ /** * crmHandoff.js — completion handoff to CRM + nurture (spec §11, §13, §17 E2). * - * The wizard is a self-contained module, so instead of mutating the global CRM - * store it writes a CRM-ready submission to localStorage. An internal page (or a - * future adapter) can read this list to create the contact/lead and fire the - * nurture playbook. Also returns the immutable compliance record (spec §18 G1). + * The wizard is a self-contained module. On completion it records a CRM-ready + * submission (read by the admin funnel analytics) and the real lead is added to the + * global mock store separately. Submissions are held in memory only — like the rest + * of the CRM demo store, nothing is written to localStorage, so the list resets on a + * full page reload. Also returns the immutable compliance record (spec §18 G1). */ -const SUBMISSIONS_KEY = 'lup_estimate_wizard_submissions_v1'; +// Module-scoped so submissions survive route changes within a session, with no +// persistence (matches the in-memory CRM store). +let submissionsStore = []; const PKG_LABEL = { GOOD: 'Good', BETTER: 'Better', BEST: 'Best' }; @@ -91,14 +94,11 @@ export function buildLeadFromSession(session, tenantConfig, reportMeta) { } export function loadSubmissions() { - try { - const raw = localStorage.getItem(SUBMISSIONS_KEY); - return raw ? JSON.parse(raw) : []; - } catch { return []; } + return submissionsStore; } function persist(list) { - try { localStorage.setItem(SUBMISSIONS_KEY, JSON.stringify(list)); } catch { /* quota */ } + submissionsStore = list; } /** diff --git a/src/modules/estimateWizard/data/tenantConfig.js b/src/modules/estimateWizard/data/tenantConfig.js index 82358e5..9bcbba1 100644 --- a/src/modules/estimateWizard/data/tenantConfig.js +++ b/src/modules/estimateWizard/data/tenantConfig.js @@ -179,13 +179,15 @@ export const DISCLAIMER_TEMPLATE = export const TENANT_CONFIG_VERSION = 'tenant-config-v1'; export const DISCLAIMER_VERSION = 'disclaimer-v1'; -// Admin-editable overrides persist here (spec §14). The wizard merges them over -// the defaults so pricing/branding/feature flags are tenant-controlled, not code. -const CONFIG_STORAGE_KEY = 'lup_estimate_wizard_config_v1'; +// Admin-editable overrides (spec §14). Held in memory only — like the rest of the +// CRM demo store, nothing is written to localStorage, so this resets on a full page +// reload. The wizard merges these over the defaults so pricing/branding/feature +// flags are tenant-controlled, not code. Module-scoped so the value survives route +// changes within a session (admin → wizard → leads) without persistence. +let configOverrides = {}; -// Same-tab change signal. The public wizard snapshots config at mount, so when the -// admin publishes in the SAME tab/SPA there's no `storage` event to react to — we -// dispatch this so an open wizard can refresh without a hard reload. +// Same-tab change signal so an open wizard refreshes when the admin publishes, +// without a hard reload (the in-memory store has no `storage` event to lean on). export const CONFIG_CHANGE_EVENT = 'lup-estimate-wizard-config-changed'; function notifyConfigChanged() { @@ -212,19 +214,16 @@ function defaults() { } export function loadConfigOverrides() { - try { - const raw = localStorage.getItem(CONFIG_STORAGE_KEY); - return raw ? JSON.parse(raw) : {}; - } catch { return {}; } + return configOverrides; } export function saveConfigOverrides(overrides) { - try { localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(overrides || {})); } catch { /* quota */ } + configOverrides = overrides || {}; notifyConfigChanged(); } export function resetConfigOverrides() { - try { localStorage.removeItem(CONFIG_STORAGE_KEY); } catch { /* ignore */ } + configOverrides = {}; notifyConfigChanged(); }