remove localstorage dependency for wizard
This commit is contained in:
+37
-23
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user