@withpotter/lib 0.13.0-next.124 → 0.13.0-next.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/admin-account-types.d.ts +0 -21
  2. package/dist/admin-api.d.ts +2 -224
  3. package/dist/admin-api.js +0 -41
  4. package/dist/admin-api.js.map +1 -1
  5. package/dist/admin-case-api.js +4 -1
  6. package/dist/admin-case-api.js.map +1 -1
  7. package/dist/admin-pos-api.d.ts +1 -25
  8. package/dist/admin-pos-api.js +0 -24
  9. package/dist/admin-pos-api.js.map +1 -1
  10. package/dist/admin-pos-types.d.ts +0 -43
  11. package/dist/api/accounts.d.ts +3 -5
  12. package/dist/api/accounts.js +1 -1
  13. package/dist/api/accounts.js.map +1 -1
  14. package/dist/api/analytics.js +3 -1
  15. package/dist/api/analytics.js.map +1 -1
  16. package/dist/api/core.d.ts +1 -0
  17. package/dist/api/core.js +7 -19
  18. package/dist/api/core.js.map +1 -1
  19. package/dist/api/potter.js +3 -2
  20. package/dist/api/potter.js.map +1 -1
  21. package/dist/api/session-scope.d.ts +2 -2
  22. package/dist/api/session-scope.js.map +1 -1
  23. package/dist/api/types/auth.d.ts +1 -7
  24. package/dist/api/types/auth.js.map +1 -1
  25. package/dist/api/types/crm.d.ts +0 -2
  26. package/dist/api/types/templates.d.ts +0 -6
  27. package/dist/auth-context.d.ts +0 -2
  28. package/dist/auth-context.js +37 -12
  29. package/dist/auth-context.js.map +1 -1
  30. package/dist/mobile-routes.js +2 -4
  31. package/dist/mobile-routes.js.map +1 -1
  32. package/dist/plan-price.d.ts +2 -16
  33. package/dist/plan-price.js +0 -9
  34. package/dist/plan-price.js.map +1 -1
  35. package/dist/resolve-enabled-behaviors.d.ts +0 -2
  36. package/dist/resolve-enabled-behaviors.js +1 -2
  37. package/dist/resolve-enabled-behaviors.js.map +1 -1
  38. package/dist/template-types.d.ts +0 -8
  39. package/dist/timeline-copy.d.ts +1 -1
  40. package/dist/timeline-copy.js +2 -6
  41. package/dist/timeline-copy.js.map +1 -1
  42. package/dist/use-nav-groups.js +5 -5
  43. package/dist/use-nav-groups.js.map +1 -1
  44. package/dist/utils.js +4 -4
  45. package/dist/utils.js.map +1 -1
  46. package/dist/wallet-format.js +0 -1
  47. package/dist/wallet-format.js.map +1 -1
  48. package/package.json +2 -2
  49. package/dist/__fixtures__/demo-content-server.json +0 -484
  50. package/dist/admin-earnings-api.d.ts +0 -90
  51. package/dist/admin-earnings-api.js +0 -15
  52. package/dist/admin-earnings-api.js.map +0 -1
  53. package/dist/admin-earnings-model.d.ts +0 -30
  54. package/dist/admin-earnings-model.js +0 -47
  55. package/dist/admin-earnings-model.js.map +0 -1
  56. package/dist/admin-pos-olive.d.ts +0 -44
  57. package/dist/admin-pos-olive.js +0 -93
  58. package/dist/admin-pos-olive.js.map +0 -1
  59. package/dist/api/demo-content.d.ts +0 -104
  60. package/dist/api/demo-content.js +0 -36
  61. package/dist/api/demo-content.js.map +0 -1
  62. package/dist/api/plan-refusal.d.ts +0 -21
  63. package/dist/api/plan-refusal.js +0 -28
  64. package/dist/api/plan-refusal.js.map +0 -1
  65. package/dist/api/session-refresh.d.ts +0 -18
  66. package/dist/api/session-refresh.js +0 -37
  67. package/dist/api/session-refresh.js.map +0 -1
  68. package/dist/demo-content-mock.d.ts +0 -22
  69. package/dist/demo-content-mock.js +0 -70
  70. package/dist/demo-content-mock.js.map +0 -1
  71. package/dist/demo-content-poller.d.ts +0 -33
  72. package/dist/demo-content-poller.js +0 -80
  73. package/dist/demo-content-poller.js.map +0 -1
  74. package/dist/demo-content.d.ts +0 -43
  75. package/dist/demo-content.js +0 -78
  76. package/dist/demo-content.js.map +0 -1
  77. package/dist/stored-token.d.ts +0 -1
  78. package/dist/stored-token.js +0 -11
  79. package/dist/stored-token.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth-context.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useState,\n useEffect,\n ReactNode,\n useCallback,\n} from 'react';\nimport {\n getApiUrl,\n apiFetch,\n accountsApi,\n tenantApi,\n type AuthSessionPayload,\n type Tenant,\n} from './api';\nimport { removeLegacyToken } from './stored-token';\nimport { resetPostHogIdentity } from '@withpotter/components/analytics';\nimport { uploadImage } from '@withpotter/lib/media-service';\nimport { endSession } from '@withpotter/lib/end-session';\nimport {\n pickWorkspace,\n readJoinedWorkspace,\n clearJoinedWorkspace,\n} from './active-workspace';\n\n// Tenant is defined once, canonically, in ./api. Re-exported here so the many\n// modules that historically imported it from auth-context keep working — there\n// is now a single Tenant shape (with the correct string id) across the app.\nexport type { Tenant };\n\nexport interface Workspace {\n tenant: Tenant;\n role: 'owner' | 'manager' | 'staff' | 'viewer';\n permissions?: Record<string, boolean>;\n}\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n avatarUrl?: string;\n /** The account's own phone number, when they gave one. */\n phone?: string | null;\n emailVerified: boolean;\n /** Two-factor authentication is active on this account. */\n mfaEnabled?: boolean;\n organizationId: string; // Created during signup\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n}\n\ninterface AuthContextType {\n user: User | null;\n tenant: Tenant | null;\n workspaces: Workspace[];\n loading: boolean;\n connectionError: boolean;\n login: (email: string, password: string) => Promise<void>;\n loginWithGoogle: (payload: AuthSessionPayload) => Promise<void>;\n loginWithCode: (\n payload: AuthSessionPayload,\n method?: SignInMethod,\n connection?: string,\n ) => Promise<void>;\n logout: () => Promise<void>;\n refreshTenant: () => Promise<void>;\n refreshUser: () => Promise<void>;\n switchWorkspace: (tenantSlug: string) => Promise<void>;\n createWorkspace: (input: CreateWorkspaceInput) => Promise<Tenant>;\n duplicateWorkspace: (\n tenantId: string,\n businessName?: string,\n ) => Promise<Tenant>;\n deleteWorkspace: (tenantSlug: string) => void;\n retryConnection: () => Promise<void>;\n}\n\n/**\n * The single input for creating a business. One shape for both entry paths\n * (first-run /onboarding and in-dashboard /workspaces/new) so every business is\n * provisioned identically. Fields not in the provision contract (salesChannel,\n * address) are persisted onto tenant.settings right after.\n */\nexport interface CreateWorkspaceInput {\n businessName: string;\n /**\n * Only for a deliberate choice of storefront look, which today means the\n * Website hub. Business creation leaves it unset: the API derives the\n * starting template from `intents`, so no client keeps a list of template ids\n * it cannot validate. Three copies of that list had already drifted.\n */\n templateId?: string;\n slug?: string;\n currency?: string;\n intents?: string[];\n isSolo?: boolean;\n category?: string;\n salesChannel?: string;\n address?: string;\n phone?: string;\n /** ISO-3166 alpha-2. Not in the provision contract, so it rides the same\n * settings patch as salesChannel. Onboarding used to collect this, derive a\n * currency from it and then throw it away — which is why Settings › General\n * showed an empty Country on every workspace ever created. */\n country?: string;\n /** Optional business logo, uploaded after provision (the media endpoint needs\n * the tenant to exist). Non-fatal — a failed upload never fails creation. */\n logoFile?: File | null;\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\n/**\n * Local-dev open access: when running `next dev` with NEXT_PUBLIC_DEV_OPEN_ACCESS=1,\n * skip auth entirely so any dashboard page is directly viewable without a\n * login/OTP (backend data just shows empty states if the API isn't running).\n * NEVER active in staging/prod — those build with NODE_ENV=production, so this\n * is false regardless of the flag.\n */\nconst DEV_OPEN_ACCESS =\n process.env.NODE_ENV === 'development' &&\n process.env.NEXT_PUBLIC_DEV_OPEN_ACCESS === '1';\n\n/**\n * The door an account came through. `connection` is a sign-in the host app provides, such as a payment or bank\n * provider's login; which one is kept beside it (see `LastAccount.connection`).\n */\nexport type SignInMethod = 'google' | 'email' | 'passkey' | 'connection';\n\n/**\n * THE PRECEDENCE RULE FOR \"WHICH WORKSPACE AM I IN\".\n *\n * `user.currentWorkspace` is the only authoritative answer. Four storage keys\n * cache it — `currentWorkspace`, `pb_current_tenant`, `tenant` and\n * `pb_last_workspace` — and they exist because consumers outside React need the\n * answer synchronously: the media proxy resolves `x-tenant-slug` straight out\n * of them (lib/media-service.ts), and the login door names the business it is\n * about to resume into before any provider has mounted.\n *\n * They are a projection of the resolved workspace, never an input to it, so\n * every path that resolves one must write all four together through here.\n * Written piecemeal they drift apart permanently: resolution falls back to\n * `workspaces[0]` whenever the stored slug is no longer in the account's tenant\n * list, and the paths that resolved by fallback used to write nothing back — so\n * storage kept naming a business the app was not in, uploads were tagged with\n * it, and the slug-keyed arrival flag never matched.\n *\n * Passing null clears the active-workspace cache without touching\n * `pb_last_workspace`, which deliberately outlives a session so a returning\n * owner resumes where they left off.\n */\nfunction persistActiveWorkspace(workspace: Workspace | null): void {\n if (!workspace) {\n localStorage.removeItem('currentWorkspace');\n localStorage.removeItem('pb_current_tenant');\n localStorage.removeItem('tenant');\n return;\n }\n const { slug } = workspace.tenant;\n localStorage.setItem('currentWorkspace', slug);\n localStorage.setItem('pb_current_tenant', slug);\n localStorage.setItem('tenant', JSON.stringify(workspace.tenant));\n localStorage.setItem('pb_last_workspace', slug);\n}\n\n/**\n * The slug to prefer when resolving the active workspace, in one order for\n * every entry path. Reload and login used to disagree on it, which let the two\n * doors into the same account land in two different businesses.\n *\n * It is a preference, not an answer: a slug that is not in the account's tenant\n * list loses to `workspaces[0]`, and the winner is written back immediately.\n */\nfunction readPreferredSlug(): string | null {\n return (\n localStorage.getItem('currentWorkspace') ||\n localStorage.getItem('pb_last_workspace') ||\n localStorage.getItem('pb_current_tenant')\n );\n}\n\n/**\n * The workspace this load lands in, for every entry path.\n *\n * Reload, login and refresh each wrote `find(slug) || workspaces[0]` inline,\n * which is why the accept-invite screen could not say \"land me in the business\n * I just joined\" — there was nowhere to say it. `pass authoritative` when the\n * list came from the API: a just-joined tenant missing from a real list is a\n * membership that no longer exists, and the handoff is dropped rather than\n * retried against every later load.\n */\nfunction resolveActiveWorkspace(\n workspaces: Workspace[],\n authoritative: boolean,\n): Workspace | null {\n const joinedTenantId = readJoinedWorkspace();\n const picked = pickWorkspace(workspaces, {\n joinedTenantId,\n preferredSlug: readPreferredSlug(),\n });\n if (\n joinedTenantId &&\n (authoritative || picked?.tenant.id === joinedTenantId)\n ) {\n clearJoinedWorkspace();\n }\n return picked;\n}\n\n/**\n * The workspace list cache, written as one unit. `workspaces` (slugs) is the\n * legacy shape, `pb_workspaces_cache` the full one; both are read as fallbacks\n * when the API is unreachable, so a business missing from either reads as a\n * business the owner does not have.\n */\nfunction persistWorkspaceList(workspaces: Workspace[]): void {\n localStorage.setItem(\n 'workspaces',\n JSON.stringify(workspaces.map((w) => w.tenant.slug)),\n );\n localStorage.setItem('pb_workspaces_cache', JSON.stringify(workspaces));\n}\n\n/**\n * Everything that makes the browser believe it is signed in.\n *\n * Hoisted out of the auth check because it was trapped in one effect's closure,\n * so any other place that discovered a dead session had no way to end it and\n * fell back to cached state instead.\n */\nfunction clearStoredAuth(): void {\n localStorage.removeItem('pb_token');\n localStorage.removeItem('pb_user');\n localStorage.removeItem('pb_account_id');\n localStorage.removeItem('userEmail');\n localStorage.removeItem('organizationId');\n persistActiveWorkspace(null);\n}\n\n/**\n * The workspace we can name WITHOUT asking the server.\n *\n * Every tenant-scoped query in the app is gated on `loading` being false, and\n * `loading` was only cleared after `/team/my-tenants` came back. So the whole\n * dashboard — twenty-odd requests on Home alone — sat waiting on one round trip\n * before it was allowed to ask for anything, on every single page load, even\n * though the answer was already sitting in localStorage from last time.\n *\n * This reads that cache so the app can start immediately. It is a HINT, not an\n * answer: `checkAuth` still calls the API and reconciles, so a workspace the\n * account has lost is corrected a moment later. Nothing is trusted for\n * authorisation either way — the API enforces access on every request, so the\n * worst case is a card that renders and then corrects itself.\n */\nfunction readCachedWorkspace(): Workspace | null {\n try {\n const raw = localStorage.getItem('pb_workspaces_cache');\n if (!raw) return null;\n const cached = JSON.parse(raw) as Workspace[];\n if (!Array.isArray(cached) || cached.length === 0) return null;\n const match =\n pickWorkspace(cached, {\n joinedTenantId: readJoinedWorkspace(),\n preferredSlug: readPreferredSlug(),\n }) ?? cached[0];\n // A slug-only legacy entry has no real id; starting on one would fire every\n // query against a tenant id that does not exist.\n return match?.tenant?.slug && match.tenant.id !== match.tenant.slug\n ? match\n : null;\n } catch {\n return null;\n }\n}\n\nexport function AuthProvider({ children }: { children: ReactNode }) {\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n const [connectionError, setConnectionError] = useState(false);\n\n const fetchTenant = useCallback(\n async (slug: string): Promise<Tenant | null> => {\n try {\n // Auth is handled via HttpOnly cookies (credentials: 'include')\n const res = await fetch(`${getApiUrl()}/tenants/${slug}`, {\n credentials: 'include',\n });\n if (!res.ok) return null;\n\n const data = await res.json();\n return {\n id: data.id,\n slug: data.slug,\n businessName: data.businessName,\n // Synthesize businessLogo from settings.logo.url (GET /tenants/:slug returns raw entity without businessLogo)\n businessLogo:\n data.businessLogo || data.settings?.logo?.url || undefined,\n contactEmail: data.contactEmail || data.email,\n contactPhone: data.contactPhone || data.phone,\n templateId: data.templateId || 'luxe-fashion-01',\n customDomain: data.customDomain || undefined,\n status: data.status,\n settings: data.settings,\n organizationId: data.organizationId,\n };\n } catch {\n return null;\n }\n },\n [],\n );\n\n // Load workspaces from API\n // Returns { workspaces, fromApi } to track if data came from API\n const loadWorkspaces = useCallback(async (): Promise<{\n workspaces: Workspace[];\n fromApi: boolean;\n }> => {\n // Auth is handled via HttpOnly cookies (credentials: 'include')\n try {\n const res = await fetch(`${getApiUrl()}/team/my-tenants`, {\n credentials: 'include',\n });\n\n // 401 means the session/token is invalid — force re-login\n if (res.status === 401 || res.status === 403) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n 'Auth expired (status',\n res.status,\n '), redirecting to login',\n );\n }\n // Clear stale auth data\n clearStoredAuth();\n // Redirect to login — throw to prevent checkAuth from updating\n // React state, which would race with this hard navigation\n if (typeof window !== 'undefined') {\n window.location.href = '/login';\n }\n throw new Error('AUTH_EXPIRED');\n }\n\n if (res.ok) {\n const apiTenants = (await res.json()) as Array<{\n id: string;\n tenantId: string;\n tenantSlug: string;\n tenantName: string;\n role: string;\n permissions?: Record<string, boolean>;\n templateId?: string | null;\n organizationId?: string | null;\n tenant?: {\n id: string;\n slug: string;\n businessName: string;\n businessLogo: string | null;\n contactEmail: string | null;\n contactPhone: string | null;\n customDomain: string | null;\n status: string;\n industryKey?: string | null;\n settings: Record<string, unknown> | null;\n };\n }>;\n\n /**\n * A DELETED WORKSPACE IS NOT A WORKSPACE.\n *\n * The membership row outlives the tenant, so deleting a workspace left\n * it in this list and therefore in the switcher. Choosing one looked\n * completely normal and then every request 401'd with \"Tenant context\n * required\" — the API's tenant middleware resolves only ACTIVE and\n * PENDING — so the whole dashboard rendered empty: no orders, no\n * customers, no analytics, no marketing. Nothing anywhere said the\n * workspace was gone, so it read as the product being broken rather\n * than the workspace not existing.\n *\n * Measured on staging: three of six workspaces in this list were\n * deleted, and all three 401 on every endpoint.\n */\n const liveTenants = apiTenants.filter(\n (t) => (t.tenant?.status ?? 'active') !== 'deleted',\n );\n\n // Use the full tenant details from the API response (no N+1 calls needed)\n const workspaces: Workspace[] = liveTenants.map((t) => ({\n tenant: {\n id: t.tenant?.id || t.tenantId,\n slug: t.tenant?.slug || t.tenantSlug,\n businessName: t.tenant?.businessName || t.tenantName,\n businessLogo: t.tenant?.businessLogo || undefined,\n contactEmail: t.tenant?.contactEmail || undefined,\n contactPhone: t.tenant?.contactPhone || undefined,\n customDomain: t.tenant?.customDomain || undefined,\n templateId: t.templateId || 'luxe-fashion-01',\n status:\n (t.tenant?.status as\n | 'active'\n | 'suspended'\n | 'pending'\n | 'deleted') || 'active',\n // What kind of business this is. The switcher labels every row\n // with it so four workspaces are four distinguishable things, and\n // dropping it here was why they all read as a bare name.\n industryKey: t.tenant?.industryKey ?? null,\n settings: t.tenant?.settings || undefined,\n organizationId: t.organizationId || undefined,\n },\n role: t.role as Workspace['role'],\n permissions: t.permissions,\n }));\n\n // Sync localStorage with API response (cache full workspace data for resilient fallback)\n if (workspaces.length > 0) {\n persistWorkspaceList(workspaces);\n }\n\n return { workspaces, fromApi: true };\n }\n\n // API returned non-ok status (e.g., 429, 500) — fall back to cached workspaces\n if (process.env.NODE_ENV === 'development') {\n console.warn('API returned error status:', res.status);\n }\n // Try full workspace cache first (has proper UUIDs)\n try {\n const cachedRaw = localStorage.getItem('pb_workspaces_cache');\n if (cachedRaw) {\n const cachedWorkspaces = JSON.parse(cachedRaw) as Workspace[];\n if (cachedWorkspaces.length > 0) {\n return { workspaces: cachedWorkspaces, fromApi: false };\n }\n }\n } catch {\n // Corrupted cache — fall through\n }\n // Legacy fallback: slugs-only cache (no UUIDs available — use connection error state)\n const cachedSlugs = JSON.parse(\n localStorage.getItem('workspaces') || '[]',\n ) as string[];\n if (cachedSlugs.length > 0) {\n const cachedWorkspaces: Workspace[] = cachedSlugs.map((slug) => ({\n tenant: {\n id: slug,\n slug,\n businessName: slug,\n templateId: 'luxe-fashion-01',\n status: 'active' as const,\n },\n role: 'owner' as const,\n }));\n return { workspaces: cachedWorkspaces, fromApi: false };\n }\n return { workspaces: [], fromApi: false };\n } catch (error) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('Failed to fetch workspaces from API:', error);\n }\n // Network error or other failure\n return { workspaces: [], fromApi: false };\n }\n }, []);\n\n useEffect(() => {\n const checkAuth = async () => {\n // Local-dev open access: bypass auth so any page renders directly.\n if (DEV_OPEN_ACCESS) {\n const slug = process.env.NEXT_PUBLIC_DEV_TENANT || 'demo';\n const tenant = {\n id: slug,\n slug,\n businessName: 'Dev Business',\n templateId: 'luxe-fashion-01',\n status: 'active' as const,\n };\n const workspace = { tenant, role: 'owner' as const };\n setUser({\n id: 'dev-user',\n email: 'dev@local',\n name: 'Dev',\n emailVerified: true,\n mfaEnabled: false,\n organizationId: '',\n workspaces: [workspace],\n currentWorkspace: workspace,\n });\n setLoading(false);\n return;\n }\n\n // The session is the HttpOnly cookie, which this code cannot see.\n // The cached account only says this browser has signed in before; the\n // calls below are the proof, and a 401 ends the session.\n removeLegacyToken();\n const pbUser = localStorage.getItem('pb_user');\n const storedOrgId = localStorage.getItem('organizationId');\n\n // No user data = invalid session\n if (!pbUser) {\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n let userData: {\n id: string;\n email: string;\n name?: string;\n avatarUrl?: string;\n phone?: string | null;\n emailVerified?: boolean;\n mfaEnabled?: boolean;\n organizationId?: string;\n } | null = null;\n try {\n userData = JSON.parse(pbUser);\n } catch {\n if (process.env.NODE_ENV === 'development') {\n console.warn('Invalid user data, clearing auth state');\n }\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n if (!userData?.email) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('No email in user data, clearing auth state');\n }\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n // ── Optimistic start ──────────────────────────────────────────────────\n // Everything tenant-scoped is gated on `loading`, so clearing it here\n // lets the dashboard begin fetching against the workspace we already\n // know, instead of idling through a round trip first. The API call below\n // still runs and reconciles; this only decides when the app is allowed\n // to START, not what is true.\n const cachedWorkspace = readCachedWorkspace();\n if (cachedWorkspace && !cancelled) {\n setUser({\n id: userData.id,\n email: userData.email,\n name: userData.name || userData.email.split('@')[0],\n avatarUrl: userData.avatarUrl || undefined,\n phone: userData.phone ?? null,\n emailVerified: userData.emailVerified ?? false,\n mfaEnabled: userData.mfaEnabled ?? false,\n organizationId:\n storedOrgId ||\n userData?.organizationId ||\n cachedWorkspace.tenant.organizationId ||\n '',\n workspaces: [cachedWorkspace],\n currentWorkspace: cachedWorkspace,\n });\n setLoading(false);\n }\n\n try {\n const freshAccount = await apiFetch<typeof userData>('/accounts/me');\n if (freshAccount) {\n userData = freshAccount;\n localStorage.setItem('pb_user', JSON.stringify(freshAccount));\n }\n } catch {\n // Keep the cached account when the API is temporarily unavailable.\n }\n\n // Load workspaces — may throw AUTH_EXPIRED if session is invalid\n let result;\n try {\n result = await loadWorkspaces();\n } catch (e: unknown) {\n if (e instanceof Error && e.message === 'AUTH_EXPIRED') return;\n throw e;\n }\n if (cancelled) return;\n const workspaces = result.workspaces;\n setConnectionError(!result.fromApi && workspaces.length === 0);\n\n const signupTenantSlug = localStorage.getItem('pb_current_tenant');\n\n // Find current workspace or use first one\n let currentWorkspace = resolveActiveWorkspace(workspaces, result.fromApi);\n\n // If no workspaces but we have signup tenant, fetch it\n if (workspaces.length === 0 && signupTenantSlug) {\n const signupTenant = await fetchTenant(signupTenantSlug);\n if (signupTenant) {\n currentWorkspace = { tenant: signupTenant, role: 'owner' };\n workspaces.push(currentWorkspace);\n persistWorkspaceList(workspaces);\n }\n }\n\n // Write the resolution back before rendering it. This is the reload path\n // that fell through with a stored slug the account no longer has: it\n // resolved `workspaces[0]` and left the cache naming the old business.\n if (currentWorkspace) persistActiveWorkspace(currentWorkspace);\n\n // Get organizationId - must be a valid UUID\n const userOrgId =\n storedOrgId ||\n userData?.organizationId ||\n currentWorkspace?.tenant?.organizationId;\n\n setUser({\n id: userData.id,\n email: userData.email,\n name: userData.name || userData.email.split('@')[0],\n avatarUrl: userData.avatarUrl || undefined,\n phone: userData.phone ?? null,\n emailVerified: userData.emailVerified ?? false,\n mfaEnabled: userData.mfaEnabled ?? false,\n organizationId: userOrgId ?? '',\n workspaces,\n currentWorkspace,\n });\n setLoading(false);\n };\n\n let cancelled = false;\n checkAuth();\n return () => {\n cancelled = true;\n };\n }, [fetchTenant, loadWorkspaces]);\n\n // Establish the client session from an auth payload (the /accounts/login\n // response, or a Google callback result of the same shape). Both the email\n // and Google doors funnel through here so they hydrate identically.\n /** Which door an account came through, remembered for the next visit. */\n // `method` is the door the account actually came through. It is stored on the\n // recognition hint so /login can offer that same door on return: greeting a\n // Google user by name and then emailing them a code is worse than not\n // recognising them at all.\n const establishSession = async (\n data: AuthSessionPayload,\n method: SignInMethod = 'email',\n connection?: string,\n ) => {\n const { account, tenants, organizationId } = data;\n\n // Build workspaces directly from enriched tenant data in the payload\n const workspaces: Workspace[] = [];\n for (const t of tenants || []) {\n workspaces.push({\n tenant: {\n id: t.id,\n slug: t.slug,\n businessName: t.name,\n businessLogo:\n t.businessLogo ||\n (t.settings as { logo?: { url?: string } } | null | undefined)?.logo\n ?.url ||\n undefined,\n contactEmail: t.contactEmail || undefined,\n contactPhone: t.contactPhone || undefined,\n customDomain: t.customDomain || undefined,\n templateId: t.templateId || 'luxe-fashion-01',\n status:\n (t.status as 'active' | 'suspended' | 'pending' | 'deleted') ||\n 'active',\n settings: t.settings || undefined,\n organizationId: t.organizationId || undefined,\n },\n role: t.role as Workspace['role'],\n permissions: t.permissions,\n });\n }\n\n // Resume the business the owner last worked in (matches the reload path),\n // falling back to the first workspace. Without this, a multi-business owner\n // whose session expired lands in an arbitrary business, not where they left.\n const signupTenantSlug = localStorage.getItem('pb_current_tenant');\n // `pb_last_workspace` deliberately survives logout (like pb_last_account),\n // so a returning owner resumes into the business they were last in rather\n // than an arbitrary first workspace — and the \"Welcome back\" card names it.\n let currentWorkspace = resolveActiveWorkspace(workspaces, true);\n if (!currentWorkspace && signupTenantSlug) {\n const signupTenant = await fetchTenant(signupTenantSlug);\n if (signupTenant) {\n currentWorkspace = { tenant: signupTenant, role: 'owner' };\n workspaces.push(currentWorkspace);\n }\n }\n\n // Get organizationId from response, or from first tenant\n const userOrgId =\n organizationId ||\n tenants?.[0]?.organizationId ||\n currentWorkspace?.tenant?.organizationId ||\n null;\n\n // Save auth data\n localStorage.setItem('pb_account_id', account.id);\n localStorage.setItem('pb_user', JSON.stringify(account));\n localStorage.setItem('userEmail', account.email);\n persistWorkspaceList(workspaces);\n if (userOrgId) {\n localStorage.setItem('organizationId', userOrgId);\n }\n persistActiveWorkspace(currentWorkspace);\n\n // Recognition hint: a small, non-sensitive record of the last account to\n // sign in on this device. Unlike the session keys above it deliberately\n // survives logout, so /login can greet a returning owner by name. It holds\n // no token — the emailed code is still the only proof of identity.\n try {\n localStorage.setItem(\n 'pb_last_account',\n JSON.stringify({\n email: account.email,\n name: account.name || account.email.split('@')[0],\n avatarUrl: account.avatarUrl || undefined,\n businessName:\n currentWorkspace?.tenant.businessName ||\n tenants?.[0]?.name ||\n undefined,\n method,\n connection: method === 'connection' ? connection : undefined,\n }),\n );\n } catch {\n // storage full / disabled — recognition is a convenience, never a gate\n }\n\n setUser({\n id: account.id,\n email: account.email,\n name: account.name || account.email.split('@')[0],\n avatarUrl: account.avatarUrl || undefined,\n phone: account.phone ?? null,\n emailVerified: account.emailVerified ?? false,\n mfaEnabled: account.mfaEnabled ?? false,\n organizationId: userOrgId ?? '',\n workspaces,\n currentWorkspace,\n });\n };\n\n const login = async (email: string, password: string) => {\n setLoading(true);\n try {\n // Call real login API\n const res = await fetch(`${getApiUrl()}/accounts/login`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n credentials: 'include', // Required to receive and send cookies\n });\n\n if (!res.ok) {\n const error = await res\n .json()\n .catch(() => ({ message: 'Login failed' }));\n throw new Error(error.message || 'Invalid email or password');\n }\n\n const data = await res.json();\n await establishSession(data);\n } finally {\n setLoading(false);\n }\n };\n\n // The Google door. The callback page already exchanged the OAuth code for a\n // session payload; here we just hydrate from it, same as email login.\n const loginWithGoogle = async (payload: AuthSessionPayload) => {\n setLoading(true);\n try {\n await establishSession(payload, 'google');\n } finally {\n setLoading(false);\n }\n };\n\n // The passwordless door. Verify already exchanged the emailed code for a\n // session payload; hydrate from it identically.\n const loginWithCode = async (\n payload: AuthSessionPayload,\n method: SignInMethod = 'email',\n connection?: string,\n ) => {\n setLoading(true);\n try {\n // The door is passed through so a passkey sign-in is REMEMBERED as one.\n // Without it the recognition card would greet a passkey user and then\n // email them a code — the exact mismatch the Google branch exists to\n // avoid, just with a different door.\n await establishSession(payload, method, connection);\n } finally {\n setLoading(false);\n }\n };\n\n const logout = async () => {\n resetPostHogIdentity();\n // The key list moved to `lib/end-session` so the invite screen can end a\n // session too, without a second copy of it drifting from this one.\n await endSession();\n setUser(null);\n };\n\n const switchWorkspace = useCallback(\n async (tenantSlug: string) => {\n if (!user) return;\n\n const workspace = user.workspaces.find(\n (w) => w.tenant.slug === tenantSlug,\n );\n if (workspace) {\n persistActiveWorkspace(workspace);\n try {\n const prev = JSON.parse(\n localStorage.getItem('pb_last_account') || '{}',\n );\n if (prev.email) {\n localStorage.setItem(\n 'pb_last_account',\n JSON.stringify({\n ...prev,\n businessName: workspace.tenant.businessName,\n }),\n );\n }\n } catch {\n // recognition hint is a convenience, never a gate\n }\n setUser((prev) =>\n prev ? { ...prev, currentWorkspace: workspace } : null,\n );\n }\n },\n [user],\n );\n\n const createWorkspace = useCallback(\n async (input: CreateWorkspaceInput): Promise<Tenant> => {\n const {\n businessName,\n templateId,\n currency,\n intents,\n isSolo,\n category,\n salesChannel,\n address,\n phone,\n country,\n logoFile,\n } = input;\n\n const slug =\n input.slug ||\n businessName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '');\n\n // organizationId is optional — server resolves it from account membership\n // when omitted; cache it for later calls.\n let organizationId = user?.organizationId || '';\n if (!organizationId) {\n const orgs = await accountsApi.getMyOrganizations();\n if (!orgs || orgs.length === 0) {\n throw new Error('No organization found. Please sign up first.');\n }\n organizationId = orgs[0].id;\n localStorage.setItem('organizationId', organizationId);\n setUser((prev) => (prev ? { ...prev, organizationId } : null));\n }\n\n // 1. Provision — the single business-creation call in the app.\n const result = await apiFetch<{ tenant: Tenant }>(\n '/onboarding/provision',\n {\n method: 'POST',\n body: JSON.stringify({\n organizationId,\n businessName,\n slug,\n // Only sent when the merchant actually chose a look. Omitted, the\n // API derives it from `intents` against the templates table it\n // owns; named, an unknown id is rejected rather than replaced.\n ...(templateId && { templateId }),\n ...(category && { category }),\n ...(intents?.length && { intents }),\n ...(isSolo != null && { isSolo }),\n ...(currency && { currency }),\n // phone is part of the provision contract — the usecase stores it\n // canonically on tenant.phone AND settings.phone, so send it here\n // rather than patching settings after (which would land it in the\n // wrong place and nothing would read it).\n ...(phone?.trim() && { phone: phone.trim() }),\n }),\n },\n );\n\n let tenant = result.tenant;\n\n // Make the new tenant the active one BEFORE the logo upload: the media\n // proxy tags the upload with `x-tenant-slug` read from stored tenant, so\n // it must point at this fresh tenant for the file to land in its namespace.\n persistActiveWorkspace({ tenant, role: 'owner' });\n\n // 2a. Upload the logo, if one was picked. The media endpoint needs the\n // tenant to exist, so this only runs post-provision. Non-fatal: a\n // failed upload must never fail business creation.\n let logoUrl: string | undefined;\n if (logoFile) {\n try {\n const uploaded = await uploadImage(logoFile);\n logoUrl = uploaded.url;\n } catch {\n // non-fatal — the merchant can add a logo later from the editor\n }\n }\n\n // 2b. Persist fields NOT in the provision contract onto settings (server\n // deep-merges). These are set here on a fresh tenant, so a shallow set\n // is safe. `logo` mirrors the editor's brand shape ({ url }), from which\n // businessLogo is synthesized. Refinements, never gates — a failure\n const settingsPatch: Record<string, unknown> = {};\n if (salesChannel) settingsPatch.salesChannel = salesChannel;\n if (intents?.length) settingsPatch.selectedIntents = intents;\n if (country) settingsPatch.country = country;\n if (address?.trim()) settingsPatch.contact = { address: address.trim() };\n if (logoUrl) settingsPatch.logo = { url: logoUrl };\n if (Object.keys(settingsPatch).length > 0) {\n try {\n await tenantApi.updateSettings(slug, settingsPatch);\n // Reflect what we just wrote onto the in-memory tenant so a soft-nav\n // dashboard reads fresh settings without a round-trip. (We merge our\n // known patch rather than the API's return, whose Tenant shape differs\n // from auth-context's.)\n tenant = {\n ...tenant,\n businessLogo: logoUrl ?? tenant.businessLogo,\n settings: { ...(tenant.settings ?? {}), ...settingsPatch },\n };\n } catch {\n // non-fatal — these refine, they don't gate\n }\n }\n\n // 3. Register in context + make active, so the dashboard renders it\n // immediately instead of bouncing back to onboarding.\n const workspace: Workspace = { tenant, role: 'owner' };\n const updatedWorkspaces = [...(user?.workspaces || []), workspace];\n persistWorkspaceList(updatedWorkspaces);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: workspace,\n }\n : null,\n );\n persistActiveWorkspace(workspace);\n\n return tenant;\n },\n [user],\n );\n\n const duplicateWorkspace = useCallback(\n async (tenantId: string, businessName?: string): Promise<Tenant> => {\n const result = await apiFetch<{ tenant: Tenant }>(\n `/onboarding/${tenantId}/duplicate`,\n {\n method: 'POST',\n body: JSON.stringify({ businessName }),\n },\n );\n\n const tenant = result.tenant;\n const workspace: Workspace = { tenant, role: 'owner' };\n const updatedWorkspaces = [...(user?.workspaces || []), workspace];\n persistWorkspaceList(updatedWorkspaces);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: workspace,\n }\n : null,\n );\n persistActiveWorkspace(workspace);\n return tenant;\n },\n [user],\n );\n\n const deleteWorkspace = useCallback(\n (tenantSlug: string) => {\n // Immediately remove the deleted workspace from in-memory state\n // so no component re-renders with a stale tenant reference\n const remaining = (user?.workspaces || []).filter(\n (w) => w.tenant.slug !== tenantSlug,\n );\n const next = remaining[0] || null;\n\n // Update localStorage to reflect the new workspace selection\n persistActiveWorkspace(next);\n if (next) {\n persistWorkspaceList(remaining);\n } else {\n localStorage.removeItem('workspaces');\n localStorage.removeItem('pb_workspaces_cache');\n }\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: remaining,\n currentWorkspace: next,\n }\n : null,\n );\n },\n [user?.workspaces],\n );\n\n const refreshUser = useCallback(async () => {\n if (!user) return;\n\n try {\n // Re-fetch account from API to get latest data (e.g. emailVerified)\n let userData: Record<string, unknown> | null = null;\n {\n try {\n const freshAccount = await apiFetch<Record<string, unknown>>(\n '/accounts/me',\n );\n if (freshAccount) {\n userData = freshAccount;\n // Update localStorage with fresh data\n localStorage.setItem('pb_user', JSON.stringify(freshAccount));\n }\n } catch (err) {\n /**\n * Two very different failures were being treated as one.\n *\n * The server being unreachable is temporary, and falling back to the\n * cached account is right: the merchant keeps working.\n *\n * The server answering \"this account does not exist\" is not\n * temporary. Swallowing that left a dead session rendering as a\n * signed-in one, reading a cached copy of a user the API has never\n * heard of. Nothing surfaced until a write went deep enough to check\n * — provisioning a business then failed with \"You do not have access\n * to this organization\", which is true, unactionable, and describes\n * the wrong problem.\n */\n const status = (err as { status?: number })?.status;\n if (status === 401 || status === 403 || status === 404) {\n clearStoredAuth();\n if (typeof window !== 'undefined') window.location.href = '/login';\n return;\n }\n // Unreachable, not refused: keep going on what we have.\n const pbUser = localStorage.getItem('pb_user');\n if (pbUser) userData = JSON.parse(pbUser);\n }\n }\n\n if (!userData) return;\n\n // Reload workspaces from API\n const result = await loadWorkspaces();\n const workspaces = result.workspaces;\n const currentWorkspace = resolveActiveWorkspace(\n workspaces,\n result.fromApi,\n );\n\n if (currentWorkspace) persistActiveWorkspace(currentWorkspace);\n\n // Get organizationId from workspace if not stored\n const storedOrgId = localStorage.getItem('organizationId');\n const orgId =\n storedOrgId ||\n currentWorkspace?.tenant?.organizationId ||\n user.organizationId;\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n name: (userData?.name as string) || prev.name,\n email: (userData?.email as string) || prev.email,\n avatarUrl: (userData?.avatarUrl as string) || prev.avatarUrl,\n // Absent from an older API response: keep what we had.\n phone:\n userData && 'phone' in userData\n ? ((userData.phone as string | null) ?? null)\n : prev.phone,\n emailVerified:\n (userData?.emailVerified as boolean) ?? prev.emailVerified,\n mfaEnabled: (userData?.mfaEnabled as boolean) ?? prev.mfaEnabled,\n workspaces,\n currentWorkspace,\n organizationId: orgId,\n }\n : null,\n );\n } catch {\n // Ignore errors\n }\n }, [user, loadWorkspaces]);\n\n const refreshTenant = useCallback(async () => {\n const currentSlug = user?.currentWorkspace?.tenant?.slug;\n const currentWorkspace = user?.currentWorkspace;\n const workspaces = user?.workspaces;\n\n if (!currentSlug || !currentWorkspace || !workspaces) return;\n\n const freshTenant = await fetchTenant(currentSlug);\n if (freshTenant) {\n const updatedWorkspace = { ...currentWorkspace, tenant: freshTenant };\n const updatedWorkspaces = workspaces.map((w) =>\n w.tenant.slug === freshTenant.slug ? updatedWorkspace : w,\n );\n persistActiveWorkspace(updatedWorkspace);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: updatedWorkspace,\n }\n : null,\n );\n }\n }, [user?.currentWorkspace, user?.workspaces, fetchTenant]);\n\n const retryConnection = useCallback(async () => {\n setLoading(true);\n setConnectionError(false);\n const result = await loadWorkspaces();\n const workspaces = result.workspaces;\n\n if (result.fromApi && workspaces.length > 0) {\n const currentWorkspace = resolveActiveWorkspace(workspaces, true);\n persistActiveWorkspace(currentWorkspace);\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces,\n currentWorkspace,\n }\n : null,\n );\n } else if (!result.fromApi) {\n setConnectionError(true);\n }\n setLoading(false);\n }, [loadWorkspaces]);\n\n return (\n <AuthContext.Provider\n value={{\n user,\n tenant: user?.currentWorkspace?.tenant ?? null,\n workspaces: user?.workspaces ?? [],\n loading,\n connectionError,\n retryConnection,\n login,\n loginWithGoogle,\n loginWithCode,\n logout,\n refreshTenant,\n refreshUser,\n switchWorkspace,\n createWorkspace,\n duplicateWorkspace,\n deleteWorkspace,\n }}\n >\n {children}\n </AuthContext.Provider>\n );\n}\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (context === undefined) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}\n\n/**\n * The current business, or null when there is no provider above.\n *\n * For shared primitives, which cannot assume they are inside an AuthProvider:\n * the root layout mounts none, each section mounts its own, and `useAuth`\n * throws. A page frame that threw would take the whole screen white rather\n * than degrade, which is the failure /auth already shipped once.\n */\nexport function useOptionalTenant(): Tenant | null {\n return useContext(AuthContext)?.tenant ?? null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/auth-context.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useState,\n useEffect,\n ReactNode,\n useCallback,\n} from 'react';\nimport {\n getApiUrl,\n apiFetch,\n accountsApi,\n tenantApi,\n type AuthSessionPayload,\n type Tenant,\n} from './api';\nimport { resetPostHogIdentity } from '@withpotter/components/analytics';\nimport { uploadImage } from '@withpotter/lib/media-service';\nimport { endSession } from '@withpotter/lib/end-session';\nimport {\n pickWorkspace,\n readJoinedWorkspace,\n clearJoinedWorkspace,\n} from './active-workspace';\n\n// Tenant is defined once, canonically, in ./api. Re-exported here so the many\n// modules that historically imported it from auth-context keep working — there\n// is now a single Tenant shape (with the correct string id) across the app.\nexport type { Tenant };\n\nexport interface Workspace {\n tenant: Tenant;\n role: 'owner' | 'manager' | 'staff' | 'viewer';\n permissions?: Record<string, boolean>;\n}\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n avatarUrl?: string;\n emailVerified: boolean;\n /** Two-factor authentication is active on this account. */\n mfaEnabled?: boolean;\n organizationId: string; // Created during signup\n workspaces: Workspace[];\n currentWorkspace: Workspace | null;\n}\n\ninterface AuthContextType {\n user: User | null;\n tenant: Tenant | null;\n workspaces: Workspace[];\n loading: boolean;\n connectionError: boolean;\n login: (email: string, password: string) => Promise<void>;\n loginWithGoogle: (payload: AuthSessionPayload) => Promise<void>;\n loginWithCode: (\n payload: AuthSessionPayload,\n method?: SignInMethod,\n connection?: string,\n ) => Promise<void>;\n logout: () => Promise<void>;\n refreshTenant: () => Promise<void>;\n refreshUser: () => Promise<void>;\n switchWorkspace: (tenantSlug: string) => Promise<void>;\n createWorkspace: (input: CreateWorkspaceInput) => Promise<Tenant>;\n duplicateWorkspace: (\n tenantId: string,\n businessName?: string,\n ) => Promise<Tenant>;\n deleteWorkspace: (tenantSlug: string) => void;\n retryConnection: () => Promise<void>;\n}\n\n/**\n * The single input for creating a business. One shape for both entry paths\n * (first-run /onboarding and in-dashboard /workspaces/new) so every business is\n * provisioned identically. Fields not in the provision contract (salesChannel,\n * address) are persisted onto tenant.settings right after.\n */\nexport interface CreateWorkspaceInput {\n businessName: string;\n /**\n * Only for a deliberate choice of storefront look, which today means the\n * Website hub. Business creation leaves it unset: the API derives the\n * starting template from `intents`, so no client keeps a list of template ids\n * it cannot validate. Three copies of that list had already drifted.\n */\n templateId?: string;\n slug?: string;\n currency?: string;\n intents?: string[];\n isSolo?: boolean;\n category?: string;\n salesChannel?: string;\n address?: string;\n phone?: string;\n /** ISO-3166 alpha-2. Not in the provision contract, so it rides the same\n * settings patch as salesChannel. Onboarding used to collect this, derive a\n * currency from it and then throw it away — which is why Settings › General\n * showed an empty Country on every workspace ever created. */\n country?: string;\n /** Optional business logo, uploaded after provision (the media endpoint needs\n * the tenant to exist). Non-fatal — a failed upload never fails creation. */\n logoFile?: File | null;\n}\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\n/**\n * Local-dev open access: when running `next dev` with NEXT_PUBLIC_DEV_OPEN_ACCESS=1,\n * skip auth entirely so any dashboard page is directly viewable without a\n * login/OTP (backend data just shows empty states if the API isn't running).\n * NEVER active in staging/prod — those build with NODE_ENV=production, so this\n * is false regardless of the flag.\n */\nconst DEV_OPEN_ACCESS =\n process.env.NODE_ENV === 'development' &&\n process.env.NEXT_PUBLIC_DEV_OPEN_ACCESS === '1';\n\n/**\n * The door an account came through. `connection` is a sign-in the host app provides, such as a payment or bank\n * provider's login; which one is kept beside it (see `LastAccount.connection`).\n */\nexport type SignInMethod = 'google' | 'email' | 'passkey' | 'connection';\n\n/**\n * THE PRECEDENCE RULE FOR \"WHICH WORKSPACE AM I IN\".\n *\n * `user.currentWorkspace` is the only authoritative answer. Four storage keys\n * cache it — `currentWorkspace`, `pb_current_tenant`, `tenant` and\n * `pb_last_workspace` — and they exist because consumers outside React need the\n * answer synchronously: the media proxy resolves `x-tenant-slug` straight out\n * of them (lib/media-service.ts), and the login door names the business it is\n * about to resume into before any provider has mounted.\n *\n * They are a projection of the resolved workspace, never an input to it, so\n * every path that resolves one must write all four together through here.\n * Written piecemeal they drift apart permanently: resolution falls back to\n * `workspaces[0]` whenever the stored slug is no longer in the account's tenant\n * list, and the paths that resolved by fallback used to write nothing back — so\n * storage kept naming a business the app was not in, uploads were tagged with\n * it, and the slug-keyed arrival flag never matched.\n *\n * Passing null clears the active-workspace cache without touching\n * `pb_last_workspace`, which deliberately outlives a session so a returning\n * owner resumes where they left off.\n */\nfunction persistActiveWorkspace(workspace: Workspace | null): void {\n if (!workspace) {\n localStorage.removeItem('currentWorkspace');\n localStorage.removeItem('pb_current_tenant');\n localStorage.removeItem('tenant');\n return;\n }\n const { slug } = workspace.tenant;\n localStorage.setItem('currentWorkspace', slug);\n localStorage.setItem('pb_current_tenant', slug);\n localStorage.setItem('tenant', JSON.stringify(workspace.tenant));\n localStorage.setItem('pb_last_workspace', slug);\n}\n\n/**\n * The slug to prefer when resolving the active workspace, in one order for\n * every entry path. Reload and login used to disagree on it, which let the two\n * doors into the same account land in two different businesses.\n *\n * It is a preference, not an answer: a slug that is not in the account's tenant\n * list loses to `workspaces[0]`, and the winner is written back immediately.\n */\nfunction readPreferredSlug(): string | null {\n return (\n localStorage.getItem('currentWorkspace') ||\n localStorage.getItem('pb_last_workspace') ||\n localStorage.getItem('pb_current_tenant')\n );\n}\n\n/**\n * The workspace this load lands in, for every entry path.\n *\n * Reload, login and refresh each wrote `find(slug) || workspaces[0]` inline,\n * which is why the accept-invite screen could not say \"land me in the business\n * I just joined\" — there was nowhere to say it. `pass authoritative` when the\n * list came from the API: a just-joined tenant missing from a real list is a\n * membership that no longer exists, and the handoff is dropped rather than\n * retried against every later load.\n */\nfunction resolveActiveWorkspace(\n workspaces: Workspace[],\n authoritative: boolean,\n): Workspace | null {\n const joinedTenantId = readJoinedWorkspace();\n const picked = pickWorkspace(workspaces, {\n joinedTenantId,\n preferredSlug: readPreferredSlug(),\n });\n if (\n joinedTenantId &&\n (authoritative || picked?.tenant.id === joinedTenantId)\n ) {\n clearJoinedWorkspace();\n }\n return picked;\n}\n\n/**\n * The workspace list cache, written as one unit. `workspaces` (slugs) is the\n * legacy shape, `pb_workspaces_cache` the full one; both are read as fallbacks\n * when the API is unreachable, so a business missing from either reads as a\n * business the owner does not have.\n */\nfunction persistWorkspaceList(workspaces: Workspace[]): void {\n localStorage.setItem(\n 'workspaces',\n JSON.stringify(workspaces.map((w) => w.tenant.slug)),\n );\n localStorage.setItem('pb_workspaces_cache', JSON.stringify(workspaces));\n}\n\n/**\n * Everything that makes the browser believe it is signed in.\n *\n * Hoisted out of the auth check because it was trapped in one effect's closure,\n * so any other place that discovered a dead session had no way to end it and\n * fell back to cached state instead.\n */\nfunction clearStoredAuth(): void {\n localStorage.removeItem('pb_token');\n localStorage.removeItem('pb_user');\n localStorage.removeItem('pb_account_id');\n localStorage.removeItem('userEmail');\n localStorage.removeItem('organizationId');\n persistActiveWorkspace(null);\n}\n\n/**\n * The workspace we can name WITHOUT asking the server.\n *\n * Every tenant-scoped query in the app is gated on `loading` being false, and\n * `loading` was only cleared after `/team/my-tenants` came back. So the whole\n * dashboard — twenty-odd requests on Home alone — sat waiting on one round trip\n * before it was allowed to ask for anything, on every single page load, even\n * though the answer was already sitting in localStorage from last time.\n *\n * This reads that cache so the app can start immediately. It is a HINT, not an\n * answer: `checkAuth` still calls the API and reconciles, so a workspace the\n * account has lost is corrected a moment later. Nothing is trusted for\n * authorisation either way — the API enforces access on every request, so the\n * worst case is a card that renders and then corrects itself.\n */\nfunction readCachedWorkspace(): Workspace | null {\n try {\n const raw = localStorage.getItem('pb_workspaces_cache');\n if (!raw) return null;\n const cached = JSON.parse(raw) as Workspace[];\n if (!Array.isArray(cached) || cached.length === 0) return null;\n const match =\n pickWorkspace(cached, {\n joinedTenantId: readJoinedWorkspace(),\n preferredSlug: readPreferredSlug(),\n }) ?? cached[0];\n // A slug-only legacy entry has no real id; starting on one would fire every\n // query against a tenant id that does not exist.\n return match?.tenant?.slug && match.tenant.id !== match.tenant.slug\n ? match\n : null;\n } catch {\n return null;\n }\n}\n\nexport function AuthProvider({ children }: { children: ReactNode }) {\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n const [connectionError, setConnectionError] = useState(false);\n\n const fetchTenant = useCallback(\n async (slug: string): Promise<Tenant | null> => {\n try {\n // Auth is handled via HttpOnly cookies (credentials: 'include')\n const res = await fetch(`${getApiUrl()}/tenants/${slug}`, {\n credentials: 'include',\n });\n if (!res.ok) return null;\n\n const data = await res.json();\n return {\n id: data.id,\n slug: data.slug,\n businessName: data.businessName,\n // Synthesize businessLogo from settings.logo.url (GET /tenants/:slug returns raw entity without businessLogo)\n businessLogo:\n data.businessLogo || data.settings?.logo?.url || undefined,\n contactEmail: data.contactEmail || data.email,\n contactPhone: data.contactPhone || data.phone,\n templateId: data.templateId || 'luxe-fashion-01',\n customDomain: data.customDomain || undefined,\n status: data.status,\n settings: data.settings,\n organizationId: data.organizationId,\n };\n } catch {\n return null;\n }\n },\n [],\n );\n\n // Load workspaces from API\n // Returns { workspaces, fromApi } to track if data came from API\n const loadWorkspaces = useCallback(async (): Promise<{\n workspaces: Workspace[];\n fromApi: boolean;\n }> => {\n // Auth is handled via HttpOnly cookies (credentials: 'include')\n try {\n const res = await fetch(`${getApiUrl()}/team/my-tenants`, {\n credentials: 'include',\n });\n\n // 401 means the session/token is invalid — force re-login\n if (res.status === 401 || res.status === 403) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n 'Auth expired (status',\n res.status,\n '), redirecting to login',\n );\n }\n // Clear stale auth data\n clearStoredAuth();\n // Redirect to login — throw to prevent checkAuth from updating\n // React state, which would race with this hard navigation\n if (typeof window !== 'undefined') {\n window.location.href = '/login';\n }\n throw new Error('AUTH_EXPIRED');\n }\n\n if (res.ok) {\n const apiTenants = (await res.json()) as Array<{\n id: string;\n tenantId: string;\n tenantSlug: string;\n tenantName: string;\n role: string;\n permissions?: Record<string, boolean>;\n templateId?: string | null;\n organizationId?: string | null;\n tenant?: {\n id: string;\n slug: string;\n businessName: string;\n businessLogo: string | null;\n contactEmail: string | null;\n contactPhone: string | null;\n customDomain: string | null;\n status: string;\n industryKey?: string | null;\n settings: Record<string, unknown> | null;\n };\n }>;\n\n /**\n * A DELETED WORKSPACE IS NOT A WORKSPACE.\n *\n * The membership row outlives the tenant, so deleting a workspace left\n * it in this list and therefore in the switcher. Choosing one looked\n * completely normal and then every request 401'd with \"Tenant context\n * required\" — the API's tenant middleware resolves only ACTIVE and\n * PENDING — so the whole dashboard rendered empty: no orders, no\n * customers, no analytics, no marketing. Nothing anywhere said the\n * workspace was gone, so it read as the product being broken rather\n * than the workspace not existing.\n *\n * Measured on staging: three of six workspaces in this list were\n * deleted, and all three 401 on every endpoint.\n */\n const liveTenants = apiTenants.filter(\n (t) => (t.tenant?.status ?? 'active') !== 'deleted',\n );\n\n // Use the full tenant details from the API response (no N+1 calls needed)\n const workspaces: Workspace[] = liveTenants.map((t) => ({\n tenant: {\n id: t.tenant?.id || t.tenantId,\n slug: t.tenant?.slug || t.tenantSlug,\n businessName: t.tenant?.businessName || t.tenantName,\n businessLogo: t.tenant?.businessLogo || undefined,\n contactEmail: t.tenant?.contactEmail || undefined,\n contactPhone: t.tenant?.contactPhone || undefined,\n customDomain: t.tenant?.customDomain || undefined,\n templateId: t.templateId || 'luxe-fashion-01',\n status:\n (t.tenant?.status as\n | 'active'\n | 'suspended'\n | 'pending'\n | 'deleted') || 'active',\n // What kind of business this is. The switcher labels every row\n // with it so four workspaces are four distinguishable things, and\n // dropping it here was why they all read as a bare name.\n industryKey: t.tenant?.industryKey ?? null,\n settings: t.tenant?.settings || undefined,\n organizationId: t.organizationId || undefined,\n },\n role: t.role as Workspace['role'],\n permissions: t.permissions,\n }));\n\n // Sync localStorage with API response (cache full workspace data for resilient fallback)\n if (workspaces.length > 0) {\n persistWorkspaceList(workspaces);\n }\n\n return { workspaces, fromApi: true };\n }\n\n // API returned non-ok status (e.g., 429, 500) — fall back to cached workspaces\n if (process.env.NODE_ENV === 'development') {\n console.warn('API returned error status:', res.status);\n }\n // Try full workspace cache first (has proper UUIDs)\n try {\n const cachedRaw = localStorage.getItem('pb_workspaces_cache');\n if (cachedRaw) {\n const cachedWorkspaces = JSON.parse(cachedRaw) as Workspace[];\n if (cachedWorkspaces.length > 0) {\n return { workspaces: cachedWorkspaces, fromApi: false };\n }\n }\n } catch {\n // Corrupted cache — fall through\n }\n // Legacy fallback: slugs-only cache (no UUIDs available — use connection error state)\n const cachedSlugs = JSON.parse(\n localStorage.getItem('workspaces') || '[]',\n ) as string[];\n if (cachedSlugs.length > 0) {\n const cachedWorkspaces: Workspace[] = cachedSlugs.map((slug) => ({\n tenant: {\n id: slug,\n slug,\n businessName: slug,\n templateId: 'luxe-fashion-01',\n status: 'active' as const,\n },\n role: 'owner' as const,\n }));\n return { workspaces: cachedWorkspaces, fromApi: false };\n }\n return { workspaces: [], fromApi: false };\n } catch (error) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('Failed to fetch workspaces from API:', error);\n }\n // Network error or other failure\n return { workspaces: [], fromApi: false };\n }\n }, []);\n\n useEffect(() => {\n const checkAuth = async () => {\n // Local-dev open access: bypass auth so any page renders directly.\n if (DEV_OPEN_ACCESS) {\n const slug = process.env.NEXT_PUBLIC_DEV_TENANT || 'demo';\n const tenant = {\n id: slug,\n slug,\n businessName: 'Dev Business',\n templateId: 'luxe-fashion-01',\n status: 'active' as const,\n };\n const workspace = { tenant, role: 'owner' as const };\n setUser({\n id: 'dev-user',\n email: 'dev@local',\n name: 'Dev',\n emailVerified: true,\n mfaEnabled: false,\n organizationId: '',\n workspaces: [workspace],\n currentWorkspace: workspace,\n });\n setLoading(false);\n return;\n }\n\n // Require valid auth token\n const token = localStorage.getItem('pb_token');\n const pbUser = localStorage.getItem('pb_user');\n const storedOrgId = localStorage.getItem('organizationId');\n\n // No token = not authenticated\n if (!token) {\n setLoading(false);\n return;\n }\n\n // Check JWT expiry client-side (7-day TTL)\n try {\n const parts = token.split('.');\n if (parts.length === 3) {\n const payload = JSON.parse(atob(parts[1]));\n if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) {\n clearStoredAuth();\n setLoading(false);\n return;\n }\n }\n } catch {\n // Malformed token — treat as expired\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n // No user data = invalid session\n if (!pbUser) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('No user data found, clearing auth state');\n }\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n let userData: {\n id: string;\n email: string;\n name?: string;\n avatarUrl?: string;\n emailVerified?: boolean;\n mfaEnabled?: boolean;\n organizationId?: string;\n } | null = null;\n try {\n userData = JSON.parse(pbUser);\n } catch {\n if (process.env.NODE_ENV === 'development') {\n console.warn('Invalid user data, clearing auth state');\n }\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n if (!userData?.email) {\n if (process.env.NODE_ENV === 'development') {\n console.warn('No email in user data, clearing auth state');\n }\n clearStoredAuth();\n setLoading(false);\n return;\n }\n\n // ── Optimistic start ──────────────────────────────────────────────────\n // Everything tenant-scoped is gated on `loading`, so clearing it here\n // lets the dashboard begin fetching against the workspace we already\n // know, instead of idling through a round trip first. The API call below\n // still runs and reconciles; this only decides when the app is allowed\n // to START, not what is true.\n const cachedWorkspace = readCachedWorkspace();\n if (cachedWorkspace && !cancelled) {\n setUser({\n id: userData.id,\n email: userData.email,\n name: userData.name || userData.email.split('@')[0],\n avatarUrl: userData.avatarUrl || undefined,\n emailVerified: userData.emailVerified ?? false,\n mfaEnabled: userData.mfaEnabled ?? false,\n organizationId:\n storedOrgId ||\n userData?.organizationId ||\n cachedWorkspace.tenant.organizationId ||\n '',\n workspaces: [cachedWorkspace],\n currentWorkspace: cachedWorkspace,\n });\n setLoading(false);\n }\n\n try {\n const freshAccount = await apiFetch<typeof userData>('/accounts/me', {\n token,\n });\n if (freshAccount) {\n userData = freshAccount;\n localStorage.setItem('pb_user', JSON.stringify(freshAccount));\n }\n } catch {\n // Keep the cached account when the API is temporarily unavailable.\n }\n\n // Load workspaces — may throw AUTH_EXPIRED if session is invalid\n let result;\n try {\n result = await loadWorkspaces();\n } catch (e: unknown) {\n if (e instanceof Error && e.message === 'AUTH_EXPIRED') return;\n throw e;\n }\n if (cancelled) return;\n const workspaces = result.workspaces;\n setConnectionError(!result.fromApi && workspaces.length === 0);\n\n const signupTenantSlug = localStorage.getItem('pb_current_tenant');\n\n // Find current workspace or use first one\n let currentWorkspace = resolveActiveWorkspace(workspaces, result.fromApi);\n\n // If no workspaces but we have signup tenant, fetch it\n if (workspaces.length === 0 && signupTenantSlug) {\n const signupTenant = await fetchTenant(signupTenantSlug);\n if (signupTenant) {\n currentWorkspace = { tenant: signupTenant, role: 'owner' };\n workspaces.push(currentWorkspace);\n persistWorkspaceList(workspaces);\n }\n }\n\n // Write the resolution back before rendering it. This is the reload path\n // that fell through with a stored slug the account no longer has: it\n // resolved `workspaces[0]` and left the cache naming the old business.\n if (currentWorkspace) persistActiveWorkspace(currentWorkspace);\n\n // Get organizationId - must be a valid UUID\n const userOrgId =\n storedOrgId ||\n userData?.organizationId ||\n currentWorkspace?.tenant?.organizationId;\n\n setUser({\n id: userData.id,\n email: userData.email,\n name: userData.name || userData.email.split('@')[0],\n avatarUrl: userData.avatarUrl || undefined,\n emailVerified: userData.emailVerified ?? false,\n mfaEnabled: userData.mfaEnabled ?? false,\n organizationId: userOrgId ?? '',\n workspaces,\n currentWorkspace,\n });\n setLoading(false);\n };\n\n let cancelled = false;\n checkAuth();\n return () => {\n cancelled = true;\n };\n }, [fetchTenant, loadWorkspaces]);\n\n // Establish the client session from an auth payload (the /accounts/login\n // response, or a Google callback result of the same shape). Both the email\n // and Google doors funnel through here so they hydrate identically.\n /** Which door an account came through, remembered for the next visit. */\n // `method` is the door the account actually came through. It is stored on the\n // recognition hint so /login can offer that same door on return: greeting a\n // Google user by name and then emailing them a code is worse than not\n // recognising them at all.\n const establishSession = async (\n data: AuthSessionPayload,\n method: SignInMethod = 'email',\n connection?: string,\n ) => {\n const { account, token, tenants, organizationId } = data;\n\n // Build workspaces directly from enriched tenant data in the payload\n const workspaces: Workspace[] = [];\n for (const t of tenants || []) {\n workspaces.push({\n tenant: {\n id: t.id,\n slug: t.slug,\n businessName: t.name,\n businessLogo:\n t.businessLogo ||\n (t.settings as { logo?: { url?: string } } | null | undefined)?.logo\n ?.url ||\n undefined,\n contactEmail: t.contactEmail || undefined,\n contactPhone: t.contactPhone || undefined,\n customDomain: t.customDomain || undefined,\n templateId: t.templateId || 'luxe-fashion-01',\n status:\n (t.status as 'active' | 'suspended' | 'pending' | 'deleted') ||\n 'active',\n settings: t.settings || undefined,\n organizationId: t.organizationId || undefined,\n },\n role: t.role as Workspace['role'],\n permissions: t.permissions,\n });\n }\n\n // Resume the business the owner last worked in (matches the reload path),\n // falling back to the first workspace. Without this, a multi-business owner\n // whose session expired lands in an arbitrary business, not where they left.\n const signupTenantSlug = localStorage.getItem('pb_current_tenant');\n // `pb_last_workspace` deliberately survives logout (like pb_last_account),\n // so a returning owner resumes into the business they were last in rather\n // than an arbitrary first workspace — and the \"Welcome back\" card names it.\n let currentWorkspace = resolveActiveWorkspace(workspaces, true);\n if (!currentWorkspace && signupTenantSlug) {\n const signupTenant = await fetchTenant(signupTenantSlug);\n if (signupTenant) {\n currentWorkspace = { tenant: signupTenant, role: 'owner' };\n workspaces.push(currentWorkspace);\n }\n }\n\n // Get organizationId from response, or from first tenant\n const userOrgId =\n organizationId ||\n tenants?.[0]?.organizationId ||\n currentWorkspace?.tenant?.organizationId ||\n null;\n\n // Save auth data\n localStorage.setItem('pb_token', token);\n localStorage.setItem('pb_account_id', account.id);\n localStorage.setItem('pb_user', JSON.stringify(account));\n localStorage.setItem('userEmail', account.email);\n persistWorkspaceList(workspaces);\n if (userOrgId) {\n localStorage.setItem('organizationId', userOrgId);\n }\n persistActiveWorkspace(currentWorkspace);\n\n // Recognition hint: a small, non-sensitive record of the last account to\n // sign in on this device. Unlike the session keys above it deliberately\n // survives logout, so /login can greet a returning owner by name. It holds\n // no token — the emailed code is still the only proof of identity.\n try {\n localStorage.setItem(\n 'pb_last_account',\n JSON.stringify({\n email: account.email,\n name: account.name || account.email.split('@')[0],\n avatarUrl: account.avatarUrl || undefined,\n businessName:\n currentWorkspace?.tenant.businessName ||\n tenants?.[0]?.name ||\n undefined,\n method,\n connection: method === 'connection' ? connection : undefined,\n }),\n );\n } catch {\n // storage full / disabled — recognition is a convenience, never a gate\n }\n\n setUser({\n id: account.id,\n email: account.email,\n name: account.name || account.email.split('@')[0],\n avatarUrl: account.avatarUrl || undefined,\n emailVerified: account.emailVerified ?? false,\n mfaEnabled: account.mfaEnabled ?? false,\n organizationId: userOrgId ?? '',\n workspaces,\n currentWorkspace,\n });\n };\n\n const login = async (email: string, password: string) => {\n setLoading(true);\n try {\n // Call real login API\n const res = await fetch(`${getApiUrl()}/accounts/login`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n credentials: 'include', // Required to receive and send cookies\n });\n\n if (!res.ok) {\n const error = await res\n .json()\n .catch(() => ({ message: 'Login failed' }));\n throw new Error(error.message || 'Invalid email or password');\n }\n\n const data = await res.json();\n await establishSession(data);\n } finally {\n setLoading(false);\n }\n };\n\n // The Google door. The callback page already exchanged the OAuth code for a\n // session payload; here we just hydrate from it, same as email login.\n const loginWithGoogle = async (payload: AuthSessionPayload) => {\n setLoading(true);\n try {\n await establishSession(payload, 'google');\n } finally {\n setLoading(false);\n }\n };\n\n // The passwordless door. Verify already exchanged the emailed code for a\n // session payload; hydrate from it identically.\n const loginWithCode = async (\n payload: AuthSessionPayload,\n method: SignInMethod = 'email',\n connection?: string,\n ) => {\n setLoading(true);\n try {\n // The door is passed through so a passkey sign-in is REMEMBERED as one.\n // Without it the recognition card would greet a passkey user and then\n // email them a code — the exact mismatch the Google branch exists to\n // avoid, just with a different door.\n await establishSession(payload, method, connection);\n } finally {\n setLoading(false);\n }\n };\n\n const logout = async () => {\n resetPostHogIdentity();\n // The key list moved to `lib/end-session` so the invite screen can end a\n // session too, without a second copy of it drifting from this one.\n await endSession();\n setUser(null);\n };\n\n const switchWorkspace = useCallback(\n async (tenantSlug: string) => {\n if (!user) return;\n\n const workspace = user.workspaces.find(\n (w) => w.tenant.slug === tenantSlug,\n );\n if (workspace) {\n persistActiveWorkspace(workspace);\n try {\n const prev = JSON.parse(\n localStorage.getItem('pb_last_account') || '{}',\n );\n if (prev.email) {\n localStorage.setItem(\n 'pb_last_account',\n JSON.stringify({\n ...prev,\n businessName: workspace.tenant.businessName,\n }),\n );\n }\n } catch {\n // recognition hint is a convenience, never a gate\n }\n setUser((prev) =>\n prev ? { ...prev, currentWorkspace: workspace } : null,\n );\n }\n },\n [user],\n );\n\n const createWorkspace = useCallback(\n async (input: CreateWorkspaceInput): Promise<Tenant> => {\n const {\n businessName,\n templateId,\n currency,\n intents,\n isSolo,\n category,\n salesChannel,\n address,\n phone,\n country,\n logoFile,\n } = input;\n\n const slug =\n input.slug ||\n businessName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '');\n\n // organizationId is optional — server resolves it from account membership\n // when omitted; cache it for later calls.\n let organizationId = user?.organizationId || '';\n if (!organizationId) {\n const token = localStorage.getItem('pb_token') || '';\n const orgs = await accountsApi.getMyOrganizations(token);\n if (!orgs || orgs.length === 0) {\n throw new Error('No organization found. Please sign up first.');\n }\n organizationId = orgs[0].id;\n localStorage.setItem('organizationId', organizationId);\n setUser((prev) => (prev ? { ...prev, organizationId } : null));\n }\n\n // 1. Provision — the single business-creation call in the app.\n const result = await apiFetch<{ tenant: Tenant }>(\n '/onboarding/provision',\n {\n method: 'POST',\n body: JSON.stringify({\n organizationId,\n businessName,\n slug,\n // Only sent when the merchant actually chose a look. Omitted, the\n // API derives it from `intents` against the templates table it\n // owns; named, an unknown id is rejected rather than replaced.\n ...(templateId && { templateId }),\n ...(category && { category }),\n ...(intents?.length && { intents }),\n ...(isSolo != null && { isSolo }),\n ...(currency && { currency }),\n // phone is part of the provision contract — the usecase stores it\n // canonically on tenant.phone AND settings.phone, so send it here\n // rather than patching settings after (which would land it in the\n // wrong place and nothing would read it).\n ...(phone?.trim() && { phone: phone.trim() }),\n }),\n },\n );\n\n let tenant = result.tenant;\n\n // Make the new tenant the active one BEFORE the logo upload: the media\n // proxy tags the upload with `x-tenant-slug` read from stored tenant, so\n // it must point at this fresh tenant for the file to land in its namespace.\n persistActiveWorkspace({ tenant, role: 'owner' });\n\n // 2a. Upload the logo, if one was picked. The media endpoint needs the\n // tenant to exist, so this only runs post-provision. Non-fatal: a\n // failed upload must never fail business creation.\n let logoUrl: string | undefined;\n if (logoFile) {\n try {\n const uploaded = await uploadImage(logoFile);\n logoUrl = uploaded.url;\n } catch {\n // non-fatal — the merchant can add a logo later from the editor\n }\n }\n\n // 2b. Persist fields NOT in the provision contract onto settings (server\n // deep-merges). These are set here on a fresh tenant, so a shallow set\n // is safe. `logo` mirrors the editor's brand shape ({ url }), from which\n // businessLogo is synthesized. Refinements, never gates — a failure\n const settingsPatch: Record<string, unknown> = {};\n if (salesChannel) settingsPatch.salesChannel = salesChannel;\n if (intents?.length) settingsPatch.selectedIntents = intents;\n if (country) settingsPatch.country = country;\n if (address?.trim()) settingsPatch.contact = { address: address.trim() };\n if (logoUrl) settingsPatch.logo = { url: logoUrl };\n if (Object.keys(settingsPatch).length > 0) {\n try {\n await tenantApi.updateSettings(slug, settingsPatch);\n // Reflect what we just wrote onto the in-memory tenant so a soft-nav\n // dashboard reads fresh settings without a round-trip. (We merge our\n // known patch rather than the API's return, whose Tenant shape differs\n // from auth-context's.)\n tenant = {\n ...tenant,\n businessLogo: logoUrl ?? tenant.businessLogo,\n settings: { ...(tenant.settings ?? {}), ...settingsPatch },\n };\n } catch {\n // non-fatal — these refine, they don't gate\n }\n }\n\n // 3. Register in context + make active, so the dashboard renders it\n // immediately instead of bouncing back to onboarding.\n const workspace: Workspace = { tenant, role: 'owner' };\n const updatedWorkspaces = [...(user?.workspaces || []), workspace];\n persistWorkspaceList(updatedWorkspaces);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: workspace,\n }\n : null,\n );\n persistActiveWorkspace(workspace);\n\n return tenant;\n },\n [user],\n );\n\n const duplicateWorkspace = useCallback(\n async (tenantId: string, businessName?: string): Promise<Tenant> => {\n const result = await apiFetch<{ tenant: Tenant }>(\n `/onboarding/${tenantId}/duplicate`,\n {\n method: 'POST',\n body: JSON.stringify({ businessName }),\n },\n );\n\n const tenant = result.tenant;\n const workspace: Workspace = { tenant, role: 'owner' };\n const updatedWorkspaces = [...(user?.workspaces || []), workspace];\n persistWorkspaceList(updatedWorkspaces);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: workspace,\n }\n : null,\n );\n persistActiveWorkspace(workspace);\n return tenant;\n },\n [user],\n );\n\n const deleteWorkspace = useCallback(\n (tenantSlug: string) => {\n // Immediately remove the deleted workspace from in-memory state\n // so no component re-renders with a stale tenant reference\n const remaining = (user?.workspaces || []).filter(\n (w) => w.tenant.slug !== tenantSlug,\n );\n const next = remaining[0] || null;\n\n // Update localStorage to reflect the new workspace selection\n persistActiveWorkspace(next);\n if (next) {\n persistWorkspaceList(remaining);\n } else {\n localStorage.removeItem('workspaces');\n localStorage.removeItem('pb_workspaces_cache');\n }\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: remaining,\n currentWorkspace: next,\n }\n : null,\n );\n },\n [user?.workspaces],\n );\n\n const refreshUser = useCallback(async () => {\n if (!user) return;\n\n try {\n // Re-fetch account from API to get latest data (e.g. emailVerified)\n const token = localStorage.getItem('pb_token');\n let userData: Record<string, unknown> | null = null;\n if (token) {\n try {\n const freshAccount = await apiFetch<Record<string, unknown>>(\n '/accounts/me',\n { token },\n );\n if (freshAccount) {\n userData = freshAccount;\n // Update localStorage with fresh data\n localStorage.setItem('pb_user', JSON.stringify(freshAccount));\n }\n } catch (err) {\n /**\n * Two very different failures were being treated as one.\n *\n * The server being unreachable is temporary, and falling back to the\n * cached account is right: the merchant keeps working.\n *\n * The server answering \"this account does not exist\" is not\n * temporary. Swallowing that left a dead session rendering as a\n * signed-in one, reading a cached copy of a user the API has never\n * heard of. Nothing surfaced until a write went deep enough to check\n * — provisioning a business then failed with \"You do not have access\n * to this organization\", which is true, unactionable, and describes\n * the wrong problem.\n */\n const status = (err as { status?: number })?.status;\n if (status === 401 || status === 403 || status === 404) {\n clearStoredAuth();\n if (typeof window !== 'undefined') window.location.href = '/login';\n return;\n }\n // Unreachable, not refused: keep going on what we have.\n const pbUser = localStorage.getItem('pb_user');\n if (pbUser) userData = JSON.parse(pbUser);\n }\n } else {\n const pbUser = localStorage.getItem('pb_user');\n if (pbUser) userData = JSON.parse(pbUser);\n }\n\n if (!userData) return;\n\n // Reload workspaces from API\n const result = await loadWorkspaces();\n const workspaces = result.workspaces;\n const currentWorkspace = resolveActiveWorkspace(\n workspaces,\n result.fromApi,\n );\n\n if (currentWorkspace) persistActiveWorkspace(currentWorkspace);\n\n // Get organizationId from workspace if not stored\n const storedOrgId = localStorage.getItem('organizationId');\n const orgId =\n storedOrgId ||\n currentWorkspace?.tenant?.organizationId ||\n user.organizationId;\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n name: (userData?.name as string) || prev.name,\n email: (userData?.email as string) || prev.email,\n avatarUrl: (userData?.avatarUrl as string) || prev.avatarUrl,\n emailVerified:\n (userData?.emailVerified as boolean) ?? prev.emailVerified,\n mfaEnabled: (userData?.mfaEnabled as boolean) ?? prev.mfaEnabled,\n workspaces,\n currentWorkspace,\n organizationId: orgId,\n }\n : null,\n );\n } catch {\n // Ignore errors\n }\n }, [user, loadWorkspaces]);\n\n const refreshTenant = useCallback(async () => {\n const currentSlug = user?.currentWorkspace?.tenant?.slug;\n const currentWorkspace = user?.currentWorkspace;\n const workspaces = user?.workspaces;\n\n if (!currentSlug || !currentWorkspace || !workspaces) return;\n\n const freshTenant = await fetchTenant(currentSlug);\n if (freshTenant) {\n const updatedWorkspace = { ...currentWorkspace, tenant: freshTenant };\n const updatedWorkspaces = workspaces.map((w) =>\n w.tenant.slug === freshTenant.slug ? updatedWorkspace : w,\n );\n persistActiveWorkspace(updatedWorkspace);\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces: updatedWorkspaces,\n currentWorkspace: updatedWorkspace,\n }\n : null,\n );\n }\n }, [user?.currentWorkspace, user?.workspaces, fetchTenant]);\n\n const retryConnection = useCallback(async () => {\n setLoading(true);\n setConnectionError(false);\n const result = await loadWorkspaces();\n const workspaces = result.workspaces;\n\n if (result.fromApi && workspaces.length > 0) {\n const currentWorkspace = resolveActiveWorkspace(workspaces, true);\n persistActiveWorkspace(currentWorkspace);\n\n setUser((prev) =>\n prev\n ? {\n ...prev,\n workspaces,\n currentWorkspace,\n }\n : null,\n );\n } else if (!result.fromApi) {\n setConnectionError(true);\n }\n setLoading(false);\n }, [loadWorkspaces]);\n\n return (\n <AuthContext.Provider\n value={{\n user,\n tenant: user?.currentWorkspace?.tenant ?? null,\n workspaces: user?.workspaces ?? [],\n loading,\n connectionError,\n retryConnection,\n login,\n loginWithGoogle,\n loginWithCode,\n logout,\n refreshTenant,\n refreshUser,\n switchWorkspace,\n createWorkspace,\n duplicateWorkspace,\n deleteWorkspace,\n }}\n >\n {children}\n </AuthContext.Provider>\n );\n}\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (context === undefined) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}\n\n/**\n * The current business, or null when there is no provider above.\n *\n * For shared primitives, which cannot assume they are inside an AuthProvider:\n * the root layout mounts none, each section mounts its own, and `useAuth`\n * throws. A page frame that threw would take the whole screen white rather\n * than degrade, which is the failure /auth already shipped once.\n */\nexport function useOptionalTenant(): Tenant | null {\n return useContext(AuthContext)?.tenant ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -248,11 +248,9 @@ const MOBILE_ROUTES = {
248
248
  "/dashboard/settings/checkout": "redirect",
249
249
  "/dashboard/settings/documents": "redirect",
250
250
  "/dashboard/settings/data-privacy": "shell",
251
- "/dashboard/settings/demo-content": "shell",
252
251
  "/dashboard/settings/devices": "shell",
253
- // Business types became Explore (2026-09-03). This path is Features again:
254
- // the switches no business type implies, Blog first.
255
- "/dashboard/settings/features": "shell",
252
+ // Business type became Explore (2026-09-03); this path only redirects.
253
+ "/dashboard/settings/features": "redirect",
256
254
  // Add more and Connectors live under Toolkit; Explore only redirects.
257
255
  "/dashboard/explore": "redirect",
258
256
  "/dashboard/toolkit": "shell",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mobile-routes.ts"],"sourcesContent":["/**\n * The mobile route contract.\n *\n * Every route under `/dashboard` declares how it behaves on a phone. There are\n * only four answers, and \"we haven't decided\" is not one of them — that is the\n * whole point of this file. `scripts/check-mobile-routes.mjs` fails CI when a\n * route exists without an entry here, or an entry here points at a route that\n * no longer exists.\n *\n * Why this exists: mobile had grown into a second app — 59 hand-built screens\n * covering 43% of the dashboard — and nothing broke when a route shipped\n * without a mobile twin. A fork with no failure mode loses ground every week.\n * This is the failure mode.\n *\n * ## The tiers\n *\n * `native` — a hand-built mobile screen under `components/mobile/screens/`,\n * drawing its own chrome from `components/mobile/primitives/chrome.tsx`\n * (`BackBar`, `BottomTabBar`). Reserved for jobs a merchant does **standing\n * up, one hand, under a minute**. That is the entire test. It is a closed\n * list; adding to it is a product decision, not a refactor. The five tab\n * roots are always native, because landing a tab press in a fallback layout\n * reads as a bug.\n *\n * `shell` — the desktop body, on a phone, with mobile chrome around it. The\n * correct answer for anything configured sitting down.\n *\n * `MobileShellChrome`, mounted in `app/dashboard/layout.tsx`, draws that\n * chrome from the live primitives. It reads `mobile-header-context`, so a\n * route names itself by publishing a title — `useScreenHeader`,\n * `useDetailHeader` or `<MobilePageHeader>`, and both page frames do it for\n * you. Publish nothing and you get a tab bar with no name on it.\n * `desktop-only` — genuinely cannot be a phone surface. Every entry carries a\n * reason so the call does not get re-litigated every six months.\n *\n * `redirect` — a stub that never renders its own UI, so it has no mobile\n * behaviour to declare. Two spellings: a server `redirect()`, or a client\n * `router.replace()` in an effect for routes that must wait on the tenant\n * before they know where to go. Tracked so the gate can tell a real gap from\n * a route that was always a signpost.\n */\n\nimport { matchRoutePattern } from './route-pattern';\n\nexport type MobileTier = 'native' | 'shell' | 'desktop-only' | 'redirect';\n\n/** Why a route can never be a phone surface. Shown nowhere; read by humans. */\nconst LAB = 'Internal design-system lab, not a merchant surface.';\nexport const DESKTOP_ONLY_REASONS: Record<string, string> = {\n '/dashboard/editor/[tenantSlug]':\n 'Canvas editor — direct manipulation of a desktop-sized artboard.',\n '/dashboard/editor':\n 'Canvas editor — direct manipulation of a desktop-sized artboard.',\n '/dashboard/buttons-lab': LAB,\n '/dashboard/charts-lab': LAB,\n '/dashboard/potter-lab': LAB,\n '/dashboard/states-lab': LAB,\n '/dashboard/design-system/fluid': LAB,\n '/dashboard/mobile-redesign':\n 'The mobile visual contract itself — a spec page for us, not a merchant surface.',\n};\n\nexport const MOBILE_ROUTES: Record<string, MobileTier> = {\n '/dashboard/design-system/fluid': 'desktop-only',\n // ── native ────────────────────────────────────────────────────────────────\n // The five tab roots first, taken from TAB_ROUTES in\n // `components/mobile/primitives/chrome.tsx` — the BottomTabBar that actually\n // renders, and now the only file that declares a tab map. Two others used to\n // (`mobile-tab-bar.tsx`, `mobile-bottom-nav.tsx`), each with a different set\n // and neither with an importer; both are deleted.\n '/dashboard': 'native', // today\n '/dashboard/orders': 'native', // manage\n '/dashboard/catalog': 'native', // catalog\n '/dashboard/store': 'native', // store\n // Me is Settings — see `app/dashboard/me/page.tsx`. Kept as an address\n // because the tab bar, the old bookmarks and `pb_recent_screens` all still\n // point at it.\n '/dashboard/me': 'redirect',\n\n // Screens the mobile IA already owns.\n // A view of the bookings surface now; this path only redirects.\n '/dashboard/today': 'redirect',\n '/dashboard/activity': 'native',\n '/dashboard/operations': 'native',\n '/dashboard/business': 'shell',\n '/dashboard/settings': 'shell',\n\n // What happened / what needs me / take the money.\n '/dashboard/analytics': 'native',\n // The hub's second view. Native because it resolves on a phone to\n // `MobileAnalyticsDrillScreen` — a drill-down the mobile IA already owned and\n // reached through `?drill=`. This is not new mobile surface area; it is the\n // same screen, addressable. Reading a report is also squarely the one-hand,\n // under-a-minute job the native tier is for.\n '/dashboard/analytics/retention': 'native',\n // The order report, which moved here from /dashboard/orders/explore. Shell,\n // not native: it is a table you scan and filter, not the one-hand job the\n // native tier is for — the retention view above earns `native` because the\n // mobile IA already owned a drill screen for it, and this has no equivalent.\n '/dashboard/analytics/orders': 'shell',\n // Attribution, which was /dashboard/marketing/attribution until 2026-08-27.\n // Shell for the same reason as the order report above: four ranked panels\n // and two range switches is a reading-and-comparing job that wants the\n // desktop body, not the one-hand native tier.\n '/dashboard/analytics/acquisition': 'shell',\n // Every module's headline numbers as stacked cards: a page you read down,\n // which is what the shell's title bar and tab bar are for.\n '/dashboard/analytics/all': 'shell',\n // What is waiting on the merchant, reached from the sales summary: stacked\n // tiles you read down, like the page above.\n '/dashboard/analytics/operations': 'shell',\n '/dashboard/payments': 'native',\n // The transaction detail has no dedicated mobile screen yet. Shell it so\n // the existing detail actions remain available instead of claiming native\n // coverage that does not exist.\n '/dashboard/payments/transactions/[transactionId]': 'shell',\n // The two other logs on the money surface. Both were `?view=` lenses of the\n // route above until they became addresses of their own; mobile draws its own\n // money screen for all three, so the tier follows the hub it sits under.\n '/dashboard/payments/payouts': 'native',\n // The disputes log is still the desktop implementation; render it through\n // the shared mobile shell until a dedicated mobile workflow exists.\n '/dashboard/payments/disputes': 'shell',\n // One payout, itemised. Shell rather than native, and it is the one route on\n // this hub that earns the difference: reconciling a deposit against a bank\n // statement is a desk job with a CSV at the end of it, not the one-hand job\n // the native tier is for. The phone gets the shell's title bar over the same\n // record the desktop draws.\n '/dashboard/payments/payouts/[payoutId]': 'shell',\n '/dashboard/money-preview': 'shell',\n // The wallet and its sections are read and set up sitting down: `shell`.\n '/dashboard/wallet': 'shell',\n '/dashboard/payments/bills': 'shell',\n '/dashboard/payments/bills/new': 'shell',\n '/dashboard/payments/bills/[id]': 'shell',\n '/dashboard/payments/vendors/[id]': 'shell',\n /**\n * Hardware. `shell`. Off the money hub on 2026-08-25, back as one row in the\n * Money group on 2026-10-05; the mobile money screen still leaves it out.\n *\n * Pairing is the job this whole surface most needs a phone for: somebody is\n * standing at a gate reading a code off a screen into a keypad. Answering\n * that with a takings summary would be a dead end at exactly the moment the\n * screen is needed. It is `shell` rather than `native` because the desktop\n * page already works one-handed — a list of rows and one dialog — so a\n * purpose-built screen would be a copy, not an improvement.\n */\n '/dashboard/hardware': 'shell',\n /** One machine. Same tier as the registry above it, for the same reason. */\n '/dashboard/hardware/[deviceId]': 'shell',\n /** Asking for hardware, and one order's journey. Desk jobs, so `shell`. */\n '/dashboard/hardware/requests': 'shell',\n '/dashboard/hardware/requests/new': 'shell',\n '/dashboard/hardware/requests/[orderId]': 'shell',\n '/dashboard/hardware/shifts/[shiftId]': 'shell',\n /**\n * Bands. `shell`, and excluded from the mobile money screen for the same\n * reason Devices is: this is counter work, done standing up with a phone in\n * one hand while somebody waits, and answering it with a takings summary\n * would be a dead end at the moment the screen is needed.\n */\n '/dashboard/payments/bands': 'shell',\n '/dashboard/orders/[orderId]': 'native',\n '/dashboard/bookings': 'native',\n '/dashboard/bookings/new': 'shell',\n // Setting a session up is a sitting-down job done once, not a thing anyone\n // does one-handed at a desk between customers.\n '/dashboard/bookings/[bookingId]': 'native',\n '/dashboard/bookings/attendees': 'native',\n '/dashboard/check-in': 'native',\n '/dashboard/customers': 'native',\n '/dashboard/customers/[customerId]': 'native',\n '/dashboard/catalog/[productId]': 'native',\n // ── desktop-only ──────────────────────────────────────────────────────────\n // Above `/dashboard/[...slug]`, which is a catch-all: declared below it, the\n // editor resolved to `shell` because the catch-all matched first.\n '/dashboard/editor': 'desktop-only',\n '/dashboard/editor/[tenantSlug]': 'desktop-only',\n '/dashboard/buttons-lab': 'desktop-only',\n '/dashboard/charts-lab': 'desktop-only',\n '/dashboard/potter-lab': 'desktop-only',\n '/dashboard/states-lab': 'desktop-only',\n '/dashboard/mobile-redesign': 'desktop-only',\n\n // ── shell ─────────────────────────────────────────────────────────────────\n '/dashboard/[...slug]': 'shell',\n '/dashboard/agency': 'shell',\n // The conversation with Box. Shell rather than native, and the tier is not\n // a placeholder: the screen is already a full-height column with a scrolling\n // transcript and a composer pinned at the foot, which is the shape a phone\n // wants anyway. What a native version would add is the keyboard handling, and\n // that is a real piece of work rather than a wrapper — it stays shell until\n // someone does it properly.\n '/dashboard/ask': 'shell',\n '/dashboard/blog': 'shell',\n '/dashboard/blog/attribution': 'shell',\n '/dashboard/blog/comments': 'shell',\n '/dashboard/blog/preview': 'shell',\n '/dashboard/build': 'shell',\n '/dashboard/builds/[buildId]': 'shell',\n '/dashboard/business/bio': 'shell',\n '/dashboard/business/checkout': 'redirect',\n '/dashboard/business/shipping': 'shell',\n '/dashboard/business/domains': 'shell',\n '/dashboard/business/domains/[domainId]': 'shell',\n '/dashboard/business/domains/add': 'shell',\n '/dashboard/business/gallery': 'shell',\n '/dashboard/business/preferences': 'shell',\n '/dashboard/business/traffic': 'shell',\n // Redirects to Settings → Operations → Calendar sync now.\n '/dashboard/calendar-sync': 'redirect',\n '/dashboard/catalog/[productId]/edit': 'shell',\n '/dashboard/catalog/new': 'shell',\n '/dashboard/categories': 'shell',\n '/dashboard/collections': 'shell',\n '/dashboard/design-system': 'shell',\n '/dashboard/collections/[collectionId]': 'shell',\n '/dashboard/collections/new': 'shell',\n '/dashboard/communications/whatsapp': 'shell',\n '/dashboard/companion': 'shell',\n '/dashboard/companion/[id]': 'shell',\n '/dashboard/content/[type]': 'shell',\n '/dashboard/content/blog/[id]': 'shell',\n '/dashboard/content/blog/[id]/edit': 'shell',\n '/dashboard/content/blog/new': 'shell',\n '/dashboard/customers/[customerId]/edit': 'shell',\n '/dashboard/customers/new': 'shell',\n '/dashboard/marketing/audiences': 'shell',\n // The builder, both ends of it. Shell: composing a segment is a two-column\n // desk job — a rule on the left, who it caught on the right — not the\n // one-hand, under-a-minute work the native tier is for.\n '/dashboard/marketing/audiences/new': 'shell',\n '/dashboard/marketing/audiences/[segmentId]': 'shell',\n '/dashboard/discounts/[discountId]': 'shell',\n '/dashboard/discounts/new': 'shell',\n '/dashboard/gift-cards/[cardId]': 'shell',\n '/dashboard/gift-cards/new': 'shell',\n '/dashboard/help': 'shell',\n // The support inbox and one case. Shell rather than native: reading a thread\n // and writing a considered reply is desk work, and the one-hand job the\n // native tier is for — \"has Box answered yet\" — is answered by the bell\n // before you ever open the list.\n '/dashboard/support': 'shell',\n '/dashboard/support/[caseId]': 'shell',\n '/dashboard/inbox': 'shell',\n '/dashboard/inquiries': 'redirect',\n '/dashboard/invoices': 'shell',\n '/dashboard/invoices/[invoiceId]': 'shell',\n '/dashboard/invoices/[invoiceId]/edit': 'shell',\n '/dashboard/invoices/new': 'shell',\n '/dashboard/loyalty': 'shell',\n '/dashboard/marketing': 'shell',\n '/dashboard/marketing/ads': 'shell',\n // A page again rather than a redirect: the always-on flows came off the top\n // of the campaigns list and back onto a row of their own. Shell because it\n // is a settings-shaped screen of switches, which is desk work.\n '/dashboard/marketing/automations': 'shell',\n // One automation's report: what it enrolled, what it completed, what it\n // earned, and the sequence it runs. Reading is desk work, so shell.\n '/dashboard/marketing/automations/[automationId]': 'shell',\n // Changing a flow's steps: a canvas of conditions and splits. Desk work.\n '/dashboard/marketing/automations/[automationId]/edit': 'shell',\n // Raising an order by hand: a customer, a product picker and a line-item\n // table. Shell like every other `/new` editor, and for the same reason the\n // segment builder is — picking products and quantities is desk work, not\n // the one-hand, under-a-minute job the native tier is for. The order you\n // then open IS native, because triaging the queue is exactly that job.\n '/dashboard/orders/new': 'shell',\n // Orders cut by a dimension, which is a reading job, not a standing-up\n // one: it wants the full desktop body and the width its bars need.\n '/dashboard/marketing/automations/abandoned-cart': 'shell',\n // Redirects to /dashboard/analytics/acquisition — the report moved to the\n // hub that measures things. The entry stays so the route is still declared.\n '/dashboard/marketing/attribution': 'redirect',\n '/dashboard/marketing/campaigns': 'shell',\n '/dashboard/marketing/campaigns/[campaignId]': 'shell',\n '/dashboard/marketing/campaigns/new': 'shell',\n '/dashboard/marketing/promotions': 'shell',\n // A list of addresses and a search: desk work, like the segments list.\n '/dashboard/marketing/opt-outs': 'shell',\n // A real page again rather than a redirect into a Loyalty tab.\n // A tab on Promotions since 2026-08-27; the path redirects.\n '/dashboard/referrals': 'redirect',\n '/dashboard/resources': 'shell',\n '/dashboard/resources/[resourceId]': 'shell',\n '/dashboard/reviews': 'redirect',\n '/dashboard/sales': 'redirect',\n '/dashboard/settings/accessibility': 'shell',\n '/dashboard/settings/activity': 'redirect',\n '/dashboard/settings/billing': 'shell',\n '/dashboard/settings/billing/credits': 'shell',\n '/dashboard/settings/billing/plan': 'shell',\n '/dashboard/settings/billing/payment-methods': 'shell',\n '/dashboard/settings/billing/rates': 'shell',\n '/dashboard/settings/billing/statements': 'shell',\n '/dashboard/settings/usage': 'shell',\n '/dashboard/settings/checkout': 'redirect',\n '/dashboard/settings/documents': 'redirect',\n '/dashboard/settings/data-privacy': 'shell',\n '/dashboard/settings/demo-content': 'shell',\n '/dashboard/settings/devices': 'shell',\n // Business types became Explore (2026-09-03). This path is Features again:\n // the switches no business type implies, Blog first.\n '/dashboard/settings/features': 'shell',\n // Add more and Connectors live under Toolkit; Explore only redirects.\n '/dashboard/explore': 'redirect',\n '/dashboard/toolkit': 'shell',\n '/dashboard/settings/branding': 'redirect',\n // Events: a list, the editor, and one night's page. Shell for all three.\n '/dashboard/events': 'shell',\n '/dashboard/events/new': 'shell',\n '/dashboard/toolkit/connectors/[id]': 'shell',\n '/dashboard/events/[eventId]': 'shell',\n '/dashboard/settings/features/navigation': 'shell',\n '/dashboard/settings/features/operating': 'shell',\n '/dashboard/settings/booking-policy': 'shell',\n '/dashboard/settings/calendar-sync': 'shell',\n // Connecting a marketplace is a sit-down job done once, with a key pasted\n // from somewhere else. The pause, which IS a standing-up one-handed act,\n // rides the orders list instead — the screen a kitchen already has open.\n '/dashboard/settings/sales-channels': 'shell',\n // Opening hours became a dialog on Business locations; this path only\n // redirects there now, so existing links still arrive somewhere.\n '/dashboard/settings/hours': 'redirect',\n '/dashboard/settings/language': 'shell',\n '/dashboard/settings/legal': 'shell',\n '/dashboard/settings/letting': 'redirect',\n '/dashboard/operations/letting': 'shell',\n '/dashboard/settings/locations': 'redirect',\n '/dashboard/operations/locations': 'shell',\n // A branch's own record: editing a location is a sit-down job.\n '/dashboard/operations/locations/[locationId]': 'shell',\n '/dashboard/settings/notifications': 'shell',\n '/dashboard/settings/notifications/email': 'shell',\n // Everything the business is plugged into, in one place instead of each\n // connection filed under the subject it affects.\n '/dashboard/settings/integrations': 'redirect',\n // One connection's health, grants and log. Read and decided sitting down: a\n // reconciliation choice needs the other system open beside it.\n '/dashboard/settings/integrations/[connectionId]': 'redirect',\n '/dashboard/toolkit/connections/[connectionId]': 'shell',\n // Payouts moved to the Payments hub; this path is a redirect stub now, and\n // the contract has a tier that says exactly that.\n '/dashboard/settings/payouts': 'redirect',\n // Where money lands moved in with what landed, on /payments/payouts.\n '/dashboard/payments/bank': 'redirect',\n /** Devices became Hardware on 2026-08-25; this path only redirects now. */\n '/dashboard/payments/devices': 'redirect',\n\n // Three rules about gift cards, set once. A sit-down job like every other\n // settings screen, reached from Promotions rather than the settings rail.\n '/dashboard/settings/gift-cards': 'shell',\n // The customer messages a merchant may reword, and one of them being edited.\n // The editor is a two-pane screen with a live email preview, which is a\n // sit-down job by definition.\n '/dashboard/settings/notifications/messages': 'shell',\n '/dashboard/settings/notifications/messages/[messageKey]': 'shell',\n // Read on a phone when something is not arriving, and the fix is pasting a\n // DNS record into a registrar — which happens on a desktop. Shell.\n '/dashboard/settings/notifications/senders': 'shell',\n '/dashboard/settings/policies': 'shell',\n '/dashboard/settings/profile': 'shell',\n // What prints on a receipt, and the two stock rules. Both are typed once and\n // left alone, which is the shell's job rather than a native screen's.\n '/dashboard/settings/receipts': 'redirect',\n '/dashboard/settings/regional': 'redirect',\n '/dashboard/settings/stock': 'redirect',\n '/dashboard/settings/roles': 'shell',\n '/dashboard/staff': 'shell',\n '/dashboard/analytics/reports': 'shell',\n '/dashboard/settings/shipping': 'redirect',\n '/dashboard/settings/taxes': 'redirect',\n '/dashboard/settings/tracking': 'redirect',\n '/dashboard/settings/team': 'shell',\n // One team member's record: role, scope, details, what they can see and what\n // they have done. Read and edited sitting down, like the roster it opens\n // from, so it takes the shell's title bar rather than a native screen.\n '/dashboard/settings/team/[memberId]': 'shell',\n '/dashboard/subscriptions': 'shell',\n '/dashboard/subscriptions/plans/[planId]/edit': 'shell',\n '/dashboard/subscriptions/plans/new': 'shell',\n '/dashboard/tabs': 'shell',\n '/dashboard/templates': 'shell',\n // Its own toolbar, its own fullscreen mode, and a viewport-sized preview\n // underneath. Chrome from the shell would sit on top of a surface whose whole\n // job is to be the customer's view of the site — and the toolbar already\n // names the template and offers a way out. Declared, and deliberately bare.\n '/dashboard/templates/preview/[templateId]': 'shell',\n '/dashboard/waitlist': 'shell',\n '/dashboard/waitlist/new': 'shell',\n '/dashboard/workspaces': 'shell',\n '/dashboard/workspaces/new': 'shell',\n\n // ── redirect ──────────────────────────────────────────────────────────────\n '/dashboard/appearance': 'redirect',\n '/dashboard/attendees': 'redirect',\n '/dashboard/blog/[id]': 'redirect',\n '/dashboard/blog/new': 'redirect',\n '/dashboard/blog_posts': 'redirect',\n '/dashboard/business/pages': 'redirect',\n '/dashboard/calendar': 'redirect',\n '/dashboard/content/blog': 'redirect',\n '/dashboard/content/blog_posts': 'redirect',\n '/dashboard/customers/analytics': 'redirect',\n // Into the Marketing hub's Discounts tab. The list stopped being a screen of\n // its own the day the sidebar row went.\n '/dashboard/discounts': 'redirect',\n // A real page again, beside the catalogue whose items issue the cards.\n '/dashboard/gift-cards': 'shell',\n '/dashboard/marketing/abandoned-cart': 'redirect',\n '/dashboard/marketing/offers': 'redirect',\n '/dashboard/finance': 'redirect',\n '/dashboard/availability': 'redirect',\n '/dashboard/bookings/sessions/new': 'redirect',\n '/dashboard/inventory': 'redirect',\n '/dashboard/inventory/[productId]': 'redirect',\n '/dashboard/inventory/[productId]/edit': 'redirect',\n '/dashboard/inventory/new': 'redirect',\n '/dashboard/marketing/email': 'redirect',\n '/dashboard/marketing/email/[id]': 'redirect',\n '/dashboard/marketing/new': 'redirect',\n '/dashboard/marketing/sms': 'redirect',\n '/dashboard/marketing/whatsapp': 'redirect',\n '/dashboard/schedule': 'redirect',\n '/dashboard/settings/domains': 'redirect',\n '/dashboard/settings/navigation': 'redirect',\n '/dashboard/settings/pages': 'redirect',\n '/dashboard/settings/email': 'redirect',\n '/dashboard/settings/help': 'redirect',\n '/dashboard/settings/seo': 'redirect',\n '/dashboard/settings/site': 'redirect',\n '/dashboard/settings/social': 'redirect',\n};\n\n/**\n * Shell-tier routes that STILL render a hand-built mobile screen.\n *\n * These are the Phase 2 deletion backlog: they failed the standing-up test, so\n * the tier says `shell`, but the screen has not been removed yet and the route\n * still branches on `useIsMobile`. Until it does, the frame must not add\n * fallback chrome on top — that is how a screen ends up with two headers.\n *\n * The list is transient and should shrink to nothing. The gate verifies it:\n * every entry must actually still branch, and no shell route may branch without\n * being listed here. Delete a mobile screen, drop its line, and the route picks\n * up the fallback automatically.\n */\n/**\n * Shell routes that deliberately draw no chrome.\n *\n * Not a gap and not a backlog: a surface whose whole job is to *be* the\n * customer's view of the site cannot have the app's furniture sitting on top of\n * it. The template preview has its own toolbar naming the template and its own\n * fullscreen mode, so it names itself and offers its own way out.\n */\nexport const CHROMELESS_BY_DESIGN: ReadonlySet<string> = new Set([\n '/dashboard/templates/preview/[templateId]',\n]);\n\nexport const LEGACY_MOBILE_SCREENS: ReadonlySet<string> = new Set([\n '/dashboard/business/domains/[domainId]',\n '/dashboard/marketing/campaigns',\n '/dashboard/settings/billing',\n]);\n\n/**\n * Whether the shared page frames should draw mobile chrome for this pathname.\n *\n * True only for a shell route that draws none of its own. A native screen\n * brings its own BackBar/BottomTabBar, and a legacy screen still does too.\n */\nexport function shouldRenderFallbackChrome(pathname: string): boolean {\n return (\n mobileTierFor(pathname) === 'shell' &&\n !LEGACY_MOBILE_SCREENS.has(pathname) &&\n !CHROMELESS_BY_DESIGN.has(pathname)\n );\n}\n\n/**\n * Resolve a live pathname to its tier, matching dynamic segments.\n *\n * Static keys win over dynamic ones, so `/dashboard/catalog/new` resolves to\n * its own entry rather than to `/dashboard/catalog/[productId]`.\n */\nexport function mobileTierFor(pathname: string): MobileTier | undefined {\n /* The walk itself lives in `route-pattern.ts` — the page-intro registry needs\n the identical match, and two copies of it would drift on the catch-all\n case without either registry looking broken. */\n const pattern = matchRoutePattern(pathname, Object.keys(MOBILE_ROUTES));\n return pattern ? MOBILE_ROUTES[pattern] : undefined;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/mobile-routes.ts"],"sourcesContent":["/**\n * The mobile route contract.\n *\n * Every route under `/dashboard` declares how it behaves on a phone. There are\n * only four answers, and \"we haven't decided\" is not one of them — that is the\n * whole point of this file. `scripts/check-mobile-routes.mjs` fails CI when a\n * route exists without an entry here, or an entry here points at a route that\n * no longer exists.\n *\n * Why this exists: mobile had grown into a second app — 59 hand-built screens\n * covering 43% of the dashboard — and nothing broke when a route shipped\n * without a mobile twin. A fork with no failure mode loses ground every week.\n * This is the failure mode.\n *\n * ## The tiers\n *\n * `native` — a hand-built mobile screen under `components/mobile/screens/`,\n * drawing its own chrome from `components/mobile/primitives/chrome.tsx`\n * (`BackBar`, `BottomTabBar`). Reserved for jobs a merchant does **standing\n * up, one hand, under a minute**. That is the entire test. It is a closed\n * list; adding to it is a product decision, not a refactor. The five tab\n * roots are always native, because landing a tab press in a fallback layout\n * reads as a bug.\n *\n * `shell` — the desktop body, on a phone, with mobile chrome around it. The\n * correct answer for anything configured sitting down.\n *\n * `MobileShellChrome`, mounted in `app/dashboard/layout.tsx`, draws that\n * chrome from the live primitives. It reads `mobile-header-context`, so a\n * route names itself by publishing a title — `useScreenHeader`,\n * `useDetailHeader` or `<MobilePageHeader>`, and both page frames do it for\n * you. Publish nothing and you get a tab bar with no name on it.\n * `desktop-only` — genuinely cannot be a phone surface. Every entry carries a\n * reason so the call does not get re-litigated every six months.\n *\n * `redirect` — a stub that never renders its own UI, so it has no mobile\n * behaviour to declare. Two spellings: a server `redirect()`, or a client\n * `router.replace()` in an effect for routes that must wait on the tenant\n * before they know where to go. Tracked so the gate can tell a real gap from\n * a route that was always a signpost.\n */\n\nimport { matchRoutePattern } from './route-pattern';\n\nexport type MobileTier = 'native' | 'shell' | 'desktop-only' | 'redirect';\n\n/** Why a route can never be a phone surface. Shown nowhere; read by humans. */\nconst LAB = 'Internal design-system lab, not a merchant surface.';\nexport const DESKTOP_ONLY_REASONS: Record<string, string> = {\n '/dashboard/editor/[tenantSlug]':\n 'Canvas editor — direct manipulation of a desktop-sized artboard.',\n '/dashboard/editor':\n 'Canvas editor — direct manipulation of a desktop-sized artboard.',\n '/dashboard/buttons-lab': LAB,\n '/dashboard/charts-lab': LAB,\n '/dashboard/potter-lab': LAB,\n '/dashboard/states-lab': LAB,\n '/dashboard/design-system/fluid': LAB,\n '/dashboard/mobile-redesign':\n 'The mobile visual contract itself — a spec page for us, not a merchant surface.',\n};\n\nexport const MOBILE_ROUTES: Record<string, MobileTier> = {\n '/dashboard/design-system/fluid': 'desktop-only',\n // ── native ────────────────────────────────────────────────────────────────\n // The five tab roots first, taken from TAB_ROUTES in\n // `components/mobile/primitives/chrome.tsx` — the BottomTabBar that actually\n // renders, and now the only file that declares a tab map. Two others used to\n // (`mobile-tab-bar.tsx`, `mobile-bottom-nav.tsx`), each with a different set\n // and neither with an importer; both are deleted.\n '/dashboard': 'native', // today\n '/dashboard/orders': 'native', // manage\n '/dashboard/catalog': 'native', // catalog\n '/dashboard/store': 'native', // store\n // Me is Settings — see `app/dashboard/me/page.tsx`. Kept as an address\n // because the tab bar, the old bookmarks and `pb_recent_screens` all still\n // point at it.\n '/dashboard/me': 'redirect',\n\n // Screens the mobile IA already owns.\n // A view of the bookings surface now; this path only redirects.\n '/dashboard/today': 'redirect',\n '/dashboard/activity': 'native',\n '/dashboard/operations': 'native',\n '/dashboard/business': 'shell',\n '/dashboard/settings': 'shell',\n\n // What happened / what needs me / take the money.\n '/dashboard/analytics': 'native',\n // The hub's second view. Native because it resolves on a phone to\n // `MobileAnalyticsDrillScreen` — a drill-down the mobile IA already owned and\n // reached through `?drill=`. This is not new mobile surface area; it is the\n // same screen, addressable. Reading a report is also squarely the one-hand,\n // under-a-minute job the native tier is for.\n '/dashboard/analytics/retention': 'native',\n // The order report, which moved here from /dashboard/orders/explore. Shell,\n // not native: it is a table you scan and filter, not the one-hand job the\n // native tier is for — the retention view above earns `native` because the\n // mobile IA already owned a drill screen for it, and this has no equivalent.\n '/dashboard/analytics/orders': 'shell',\n // Attribution, which was /dashboard/marketing/attribution until 2026-08-27.\n // Shell for the same reason as the order report above: four ranked panels\n // and two range switches is a reading-and-comparing job that wants the\n // desktop body, not the one-hand native tier.\n '/dashboard/analytics/acquisition': 'shell',\n // Every module's headline numbers as stacked cards: a page you read down,\n // which is what the shell's title bar and tab bar are for.\n '/dashboard/analytics/all': 'shell',\n // What is waiting on the merchant, reached from the sales summary: stacked\n // tiles you read down, like the page above.\n '/dashboard/analytics/operations': 'shell',\n '/dashboard/payments': 'native',\n // The transaction detail has no dedicated mobile screen yet. Shell it so\n // the existing detail actions remain available instead of claiming native\n // coverage that does not exist.\n '/dashboard/payments/transactions/[transactionId]': 'shell',\n // The two other logs on the money surface. Both were `?view=` lenses of the\n // route above until they became addresses of their own; mobile draws its own\n // money screen for all three, so the tier follows the hub it sits under.\n '/dashboard/payments/payouts': 'native',\n // The disputes log is still the desktop implementation; render it through\n // the shared mobile shell until a dedicated mobile workflow exists.\n '/dashboard/payments/disputes': 'shell',\n // One payout, itemised. Shell rather than native, and it is the one route on\n // this hub that earns the difference: reconciling a deposit against a bank\n // statement is a desk job with a CSV at the end of it, not the one-hand job\n // the native tier is for. The phone gets the shell's title bar over the same\n // record the desktop draws.\n '/dashboard/payments/payouts/[payoutId]': 'shell',\n '/dashboard/money-preview': 'shell',\n // The wallet and its sections are read and set up sitting down: `shell`.\n '/dashboard/wallet': 'shell',\n '/dashboard/payments/bills': 'shell',\n '/dashboard/payments/bills/new': 'shell',\n '/dashboard/payments/bills/[id]': 'shell',\n '/dashboard/payments/vendors/[id]': 'shell',\n /**\n * Hardware. `shell`. Off the money hub on 2026-08-25, back as one row in the\n * Money group on 2026-10-05; the mobile money screen still leaves it out.\n *\n * Pairing is the job this whole surface most needs a phone for: somebody is\n * standing at a gate reading a code off a screen into a keypad. Answering\n * that with a takings summary would be a dead end at exactly the moment the\n * screen is needed. It is `shell` rather than `native` because the desktop\n * page already works one-handed — a list of rows and one dialog — so a\n * purpose-built screen would be a copy, not an improvement.\n */\n '/dashboard/hardware': 'shell',\n /** One machine. Same tier as the registry above it, for the same reason. */\n '/dashboard/hardware/[deviceId]': 'shell',\n /** Asking for hardware, and one order's journey. Desk jobs, so `shell`. */\n '/dashboard/hardware/requests': 'shell',\n '/dashboard/hardware/requests/new': 'shell',\n '/dashboard/hardware/requests/[orderId]': 'shell',\n '/dashboard/hardware/shifts/[shiftId]': 'shell',\n /**\n * Bands. `shell`, and excluded from the mobile money screen for the same\n * reason Devices is: this is counter work, done standing up with a phone in\n * one hand while somebody waits, and answering it with a takings summary\n * would be a dead end at the moment the screen is needed.\n */\n '/dashboard/payments/bands': 'shell',\n '/dashboard/orders/[orderId]': 'native',\n '/dashboard/bookings': 'native',\n '/dashboard/bookings/new': 'shell',\n // Setting a session up is a sitting-down job done once, not a thing anyone\n // does one-handed at a desk between customers.\n '/dashboard/bookings/[bookingId]': 'native',\n '/dashboard/bookings/attendees': 'native',\n '/dashboard/check-in': 'native',\n '/dashboard/customers': 'native',\n '/dashboard/customers/[customerId]': 'native',\n '/dashboard/catalog/[productId]': 'native',\n // ── desktop-only ──────────────────────────────────────────────────────────\n // Above `/dashboard/[...slug]`, which is a catch-all: declared below it, the\n // editor resolved to `shell` because the catch-all matched first.\n '/dashboard/editor': 'desktop-only',\n '/dashboard/editor/[tenantSlug]': 'desktop-only',\n '/dashboard/buttons-lab': 'desktop-only',\n '/dashboard/charts-lab': 'desktop-only',\n '/dashboard/potter-lab': 'desktop-only',\n '/dashboard/states-lab': 'desktop-only',\n '/dashboard/mobile-redesign': 'desktop-only',\n\n // ── shell ─────────────────────────────────────────────────────────────────\n '/dashboard/[...slug]': 'shell',\n '/dashboard/agency': 'shell',\n // The conversation with Box. Shell rather than native, and the tier is not\n // a placeholder: the screen is already a full-height column with a scrolling\n // transcript and a composer pinned at the foot, which is the shape a phone\n // wants anyway. What a native version would add is the keyboard handling, and\n // that is a real piece of work rather than a wrapper — it stays shell until\n // someone does it properly.\n '/dashboard/ask': 'shell',\n '/dashboard/blog': 'shell',\n '/dashboard/blog/attribution': 'shell',\n '/dashboard/blog/comments': 'shell',\n '/dashboard/blog/preview': 'shell',\n '/dashboard/build': 'shell',\n '/dashboard/builds/[buildId]': 'shell',\n '/dashboard/business/bio': 'shell',\n '/dashboard/business/checkout': 'redirect',\n '/dashboard/business/shipping': 'shell',\n '/dashboard/business/domains': 'shell',\n '/dashboard/business/domains/[domainId]': 'shell',\n '/dashboard/business/domains/add': 'shell',\n '/dashboard/business/gallery': 'shell',\n '/dashboard/business/preferences': 'shell',\n '/dashboard/business/traffic': 'shell',\n // Redirects to Settings → Operations → Calendar sync now.\n '/dashboard/calendar-sync': 'redirect',\n '/dashboard/catalog/[productId]/edit': 'shell',\n '/dashboard/catalog/new': 'shell',\n '/dashboard/categories': 'shell',\n '/dashboard/collections': 'shell',\n '/dashboard/design-system': 'shell',\n '/dashboard/collections/[collectionId]': 'shell',\n '/dashboard/collections/new': 'shell',\n '/dashboard/communications/whatsapp': 'shell',\n '/dashboard/companion': 'shell',\n '/dashboard/companion/[id]': 'shell',\n '/dashboard/content/[type]': 'shell',\n '/dashboard/content/blog/[id]': 'shell',\n '/dashboard/content/blog/[id]/edit': 'shell',\n '/dashboard/content/blog/new': 'shell',\n '/dashboard/customers/[customerId]/edit': 'shell',\n '/dashboard/customers/new': 'shell',\n '/dashboard/marketing/audiences': 'shell',\n // The builder, both ends of it. Shell: composing a segment is a two-column\n // desk job — a rule on the left, who it caught on the right — not the\n // one-hand, under-a-minute work the native tier is for.\n '/dashboard/marketing/audiences/new': 'shell',\n '/dashboard/marketing/audiences/[segmentId]': 'shell',\n '/dashboard/discounts/[discountId]': 'shell',\n '/dashboard/discounts/new': 'shell',\n '/dashboard/gift-cards/[cardId]': 'shell',\n '/dashboard/gift-cards/new': 'shell',\n '/dashboard/help': 'shell',\n // The support inbox and one case. Shell rather than native: reading a thread\n // and writing a considered reply is desk work, and the one-hand job the\n // native tier is for — \"has Box answered yet\" — is answered by the bell\n // before you ever open the list.\n '/dashboard/support': 'shell',\n '/dashboard/support/[caseId]': 'shell',\n '/dashboard/inbox': 'shell',\n '/dashboard/inquiries': 'redirect',\n '/dashboard/invoices': 'shell',\n '/dashboard/invoices/[invoiceId]': 'shell',\n '/dashboard/invoices/[invoiceId]/edit': 'shell',\n '/dashboard/invoices/new': 'shell',\n '/dashboard/loyalty': 'shell',\n '/dashboard/marketing': 'shell',\n '/dashboard/marketing/ads': 'shell',\n // A page again rather than a redirect: the always-on flows came off the top\n // of the campaigns list and back onto a row of their own. Shell because it\n // is a settings-shaped screen of switches, which is desk work.\n '/dashboard/marketing/automations': 'shell',\n // One automation's report: what it enrolled, what it completed, what it\n // earned, and the sequence it runs. Reading is desk work, so shell.\n '/dashboard/marketing/automations/[automationId]': 'shell',\n // Changing a flow's steps: a canvas of conditions and splits. Desk work.\n '/dashboard/marketing/automations/[automationId]/edit': 'shell',\n // Raising an order by hand: a customer, a product picker and a line-item\n // table. Shell like every other `/new` editor, and for the same reason the\n // segment builder is — picking products and quantities is desk work, not\n // the one-hand, under-a-minute job the native tier is for. The order you\n // then open IS native, because triaging the queue is exactly that job.\n '/dashboard/orders/new': 'shell',\n // Orders cut by a dimension, which is a reading job, not a standing-up\n // one: it wants the full desktop body and the width its bars need.\n '/dashboard/marketing/automations/abandoned-cart': 'shell',\n // Redirects to /dashboard/analytics/acquisition — the report moved to the\n // hub that measures things. The entry stays so the route is still declared.\n '/dashboard/marketing/attribution': 'redirect',\n '/dashboard/marketing/campaigns': 'shell',\n '/dashboard/marketing/campaigns/[campaignId]': 'shell',\n '/dashboard/marketing/campaigns/new': 'shell',\n '/dashboard/marketing/promotions': 'shell',\n // A list of addresses and a search: desk work, like the segments list.\n '/dashboard/marketing/opt-outs': 'shell',\n // A real page again rather than a redirect into a Loyalty tab.\n // A tab on Promotions since 2026-08-27; the path redirects.\n '/dashboard/referrals': 'redirect',\n '/dashboard/resources': 'shell',\n '/dashboard/resources/[resourceId]': 'shell',\n '/dashboard/reviews': 'redirect',\n '/dashboard/sales': 'redirect',\n '/dashboard/settings/accessibility': 'shell',\n '/dashboard/settings/activity': 'redirect',\n '/dashboard/settings/billing': 'shell',\n '/dashboard/settings/billing/credits': 'shell',\n '/dashboard/settings/billing/plan': 'shell',\n '/dashboard/settings/billing/payment-methods': 'shell',\n '/dashboard/settings/billing/rates': 'shell',\n '/dashboard/settings/billing/statements': 'shell',\n '/dashboard/settings/usage': 'shell',\n '/dashboard/settings/checkout': 'redirect',\n '/dashboard/settings/documents': 'redirect',\n '/dashboard/settings/data-privacy': 'shell',\n '/dashboard/settings/devices': 'shell',\n // Business type became Explore (2026-09-03); this path only redirects.\n '/dashboard/settings/features': 'redirect',\n // Add more and Connectors live under Toolkit; Explore only redirects.\n '/dashboard/explore': 'redirect',\n '/dashboard/toolkit': 'shell',\n '/dashboard/settings/branding': 'redirect',\n // Events: a list, the editor, and one night's page. Shell for all three.\n '/dashboard/events': 'shell',\n '/dashboard/events/new': 'shell',\n '/dashboard/toolkit/connectors/[id]': 'shell',\n '/dashboard/events/[eventId]': 'shell',\n '/dashboard/settings/features/navigation': 'shell',\n '/dashboard/settings/features/operating': 'shell',\n '/dashboard/settings/booking-policy': 'shell',\n '/dashboard/settings/calendar-sync': 'shell',\n // Connecting a marketplace is a sit-down job done once, with a key pasted\n // from somewhere else. The pause, which IS a standing-up one-handed act,\n // rides the orders list instead — the screen a kitchen already has open.\n '/dashboard/settings/sales-channels': 'shell',\n // Opening hours became a dialog on Business locations; this path only\n // redirects there now, so existing links still arrive somewhere.\n '/dashboard/settings/hours': 'redirect',\n '/dashboard/settings/language': 'shell',\n '/dashboard/settings/legal': 'shell',\n '/dashboard/settings/letting': 'redirect',\n '/dashboard/operations/letting': 'shell',\n '/dashboard/settings/locations': 'redirect',\n '/dashboard/operations/locations': 'shell',\n // A branch's own record: editing a location is a sit-down job.\n '/dashboard/operations/locations/[locationId]': 'shell',\n '/dashboard/settings/notifications': 'shell',\n '/dashboard/settings/notifications/email': 'shell',\n // Everything the business is plugged into, in one place instead of each\n // connection filed under the subject it affects.\n '/dashboard/settings/integrations': 'redirect',\n // One connection's health, grants and log. Read and decided sitting down: a\n // reconciliation choice needs the other system open beside it.\n '/dashboard/settings/integrations/[connectionId]': 'redirect',\n '/dashboard/toolkit/connections/[connectionId]': 'shell',\n // Payouts moved to the Payments hub; this path is a redirect stub now, and\n // the contract has a tier that says exactly that.\n '/dashboard/settings/payouts': 'redirect',\n // Where money lands moved in with what landed, on /payments/payouts.\n '/dashboard/payments/bank': 'redirect',\n /** Devices became Hardware on 2026-08-25; this path only redirects now. */\n '/dashboard/payments/devices': 'redirect',\n\n // Three rules about gift cards, set once. A sit-down job like every other\n // settings screen, reached from Promotions rather than the settings rail.\n '/dashboard/settings/gift-cards': 'shell',\n // The customer messages a merchant may reword, and one of them being edited.\n // The editor is a two-pane screen with a live email preview, which is a\n // sit-down job by definition.\n '/dashboard/settings/notifications/messages': 'shell',\n '/dashboard/settings/notifications/messages/[messageKey]': 'shell',\n // Read on a phone when something is not arriving, and the fix is pasting a\n // DNS record into a registrar — which happens on a desktop. Shell.\n '/dashboard/settings/notifications/senders': 'shell',\n '/dashboard/settings/policies': 'shell',\n '/dashboard/settings/profile': 'shell',\n // What prints on a receipt, and the two stock rules. Both are typed once and\n // left alone, which is the shell's job rather than a native screen's.\n '/dashboard/settings/receipts': 'redirect',\n '/dashboard/settings/regional': 'redirect',\n '/dashboard/settings/stock': 'redirect',\n '/dashboard/settings/roles': 'shell',\n '/dashboard/staff': 'shell',\n '/dashboard/analytics/reports': 'shell',\n '/dashboard/settings/shipping': 'redirect',\n '/dashboard/settings/taxes': 'redirect',\n '/dashboard/settings/tracking': 'redirect',\n '/dashboard/settings/team': 'shell',\n // One team member's record: role, scope, details, what they can see and what\n // they have done. Read and edited sitting down, like the roster it opens\n // from, so it takes the shell's title bar rather than a native screen.\n '/dashboard/settings/team/[memberId]': 'shell',\n '/dashboard/subscriptions': 'shell',\n '/dashboard/subscriptions/plans/[planId]/edit': 'shell',\n '/dashboard/subscriptions/plans/new': 'shell',\n '/dashboard/tabs': 'shell',\n '/dashboard/templates': 'shell',\n // Its own toolbar, its own fullscreen mode, and a viewport-sized preview\n // underneath. Chrome from the shell would sit on top of a surface whose whole\n // job is to be the customer's view of the site — and the toolbar already\n // names the template and offers a way out. Declared, and deliberately bare.\n '/dashboard/templates/preview/[templateId]': 'shell',\n '/dashboard/waitlist': 'shell',\n '/dashboard/waitlist/new': 'shell',\n '/dashboard/workspaces': 'shell',\n '/dashboard/workspaces/new': 'shell',\n\n // ── redirect ──────────────────────────────────────────────────────────────\n '/dashboard/appearance': 'redirect',\n '/dashboard/attendees': 'redirect',\n '/dashboard/blog/[id]': 'redirect',\n '/dashboard/blog/new': 'redirect',\n '/dashboard/blog_posts': 'redirect',\n '/dashboard/business/pages': 'redirect',\n '/dashboard/calendar': 'redirect',\n '/dashboard/content/blog': 'redirect',\n '/dashboard/content/blog_posts': 'redirect',\n '/dashboard/customers/analytics': 'redirect',\n // Into the Marketing hub's Discounts tab. The list stopped being a screen of\n // its own the day the sidebar row went.\n '/dashboard/discounts': 'redirect',\n // A real page again, beside the catalogue whose items issue the cards.\n '/dashboard/gift-cards': 'shell',\n '/dashboard/marketing/abandoned-cart': 'redirect',\n '/dashboard/marketing/offers': 'redirect',\n '/dashboard/finance': 'redirect',\n '/dashboard/availability': 'redirect',\n '/dashboard/bookings/sessions/new': 'redirect',\n '/dashboard/inventory': 'redirect',\n '/dashboard/inventory/[productId]': 'redirect',\n '/dashboard/inventory/[productId]/edit': 'redirect',\n '/dashboard/inventory/new': 'redirect',\n '/dashboard/marketing/email': 'redirect',\n '/dashboard/marketing/email/[id]': 'redirect',\n '/dashboard/marketing/new': 'redirect',\n '/dashboard/marketing/sms': 'redirect',\n '/dashboard/marketing/whatsapp': 'redirect',\n '/dashboard/schedule': 'redirect',\n '/dashboard/settings/domains': 'redirect',\n '/dashboard/settings/navigation': 'redirect',\n '/dashboard/settings/pages': 'redirect',\n '/dashboard/settings/email': 'redirect',\n '/dashboard/settings/help': 'redirect',\n '/dashboard/settings/seo': 'redirect',\n '/dashboard/settings/site': 'redirect',\n '/dashboard/settings/social': 'redirect',\n};\n\n/**\n * Shell-tier routes that STILL render a hand-built mobile screen.\n *\n * These are the Phase 2 deletion backlog: they failed the standing-up test, so\n * the tier says `shell`, but the screen has not been removed yet and the route\n * still branches on `useIsMobile`. Until it does, the frame must not add\n * fallback chrome on top — that is how a screen ends up with two headers.\n *\n * The list is transient and should shrink to nothing. The gate verifies it:\n * every entry must actually still branch, and no shell route may branch without\n * being listed here. Delete a mobile screen, drop its line, and the route picks\n * up the fallback automatically.\n */\n/**\n * Shell routes that deliberately draw no chrome.\n *\n * Not a gap and not a backlog: a surface whose whole job is to *be* the\n * customer's view of the site cannot have the app's furniture sitting on top of\n * it. The template preview has its own toolbar naming the template and its own\n * fullscreen mode, so it names itself and offers its own way out.\n */\nexport const CHROMELESS_BY_DESIGN: ReadonlySet<string> = new Set([\n '/dashboard/templates/preview/[templateId]',\n]);\n\nexport const LEGACY_MOBILE_SCREENS: ReadonlySet<string> = new Set([\n '/dashboard/business/domains/[domainId]',\n '/dashboard/marketing/campaigns',\n '/dashboard/settings/billing',\n]);\n\n/**\n * Whether the shared page frames should draw mobile chrome for this pathname.\n *\n * True only for a shell route that draws none of its own. A native screen\n * brings its own BackBar/BottomTabBar, and a legacy screen still does too.\n */\nexport function shouldRenderFallbackChrome(pathname: string): boolean {\n return (\n mobileTierFor(pathname) === 'shell' &&\n !LEGACY_MOBILE_SCREENS.has(pathname) &&\n !CHROMELESS_BY_DESIGN.has(pathname)\n );\n}\n\n/**\n * Resolve a live pathname to its tier, matching dynamic segments.\n *\n * Static keys win over dynamic ones, so `/dashboard/catalog/new` resolves to\n * its own entry rather than to `/dashboard/catalog/[productId]`.\n */\nexport function mobileTierFor(pathname: string): MobileTier | undefined {\n /* The walk itself lives in `route-pattern.ts` — the page-intro registry needs\n the identical match, and two copies of it would drift on the catch-all\n case without either registry looking broken. */\n const pattern = matchRoutePattern(pathname, Object.keys(MOBILE_ROUTES));\n return pattern ? MOBILE_ROUTES[pattern] : undefined;\n}\n"],"mappings":"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","names":[]}
@@ -7,22 +7,8 @@ export declare function formatPlanPrice(minor: string, currency: string, exponen
7
7
  /** NGN first, then the rest by code. */
8
8
  export declare const byCurrency: (a: string, b: string) => number;
9
9
  /**
10
- * The one price to show for a plan whose prices are plain numbers in major
11
- * units: naira when the plan has a naira price, then the preferred currency
12
- * if it has one, then the first other currency by code. Never a conversion,
13
- * and the currency comes back with the amount so it is never shown under
14
- * another currency's sign.
15
- */
16
- export declare function pickPlanPrice(prices: Record<string, number> | null | undefined, preferred?: string | null): {
17
- currency: string;
18
- amount: number;
19
- } | null;
20
- /**
21
- * The price lines to show. Asked for one currency (the single price a
22
- * merchant sees): a plan with a naira price shows just that, else the
23
- * currency asked for if the plan has it, else every currency it has. Asked
24
- * for none, every currency it has, one line each, naira first. The exponent
25
- * only applies to the currency asked for.
10
+ * The price lines to show: the one currency asked for when the plan has a
11
+ * price in it, otherwise every currency it has, one line each.
26
12
  */
27
13
  export declare function planPriceLines(prices: Prices, currency?: string | null, exponent?: number | null): string[];
28
14
  /** "₦15,000 a month", or "₦15,000 a month · $10 a month" across currencies. */
@@ -23,15 +23,7 @@ function formatPlanPrice(minor, currency, exponent) {
23
23
  return text.replace(/[.,]0+(?=\D*$)/, "");
24
24
  }
25
25
  const byCurrency = (a, b) => a === "NGN" ? -1 : b === "NGN" ? 1 : a.localeCompare(b);
26
- function pickPlanPrice(prices, preferred) {
27
- if (!prices) return null;
28
- const code = prices["NGN"] !== void 0 ? "NGN" : preferred && prices[preferred] !== void 0 ? preferred : Object.keys(prices).sort((a, b) => a.localeCompare(b))[0];
29
- return code === void 0 ? null : { currency: code, amount: prices[code] };
30
- }
31
26
  function planPriceLines(prices, currency, exponent) {
32
- if (currency && prices["NGN"] !== void 0) {
33
- return [formatPlanPrice(prices["NGN"], "NGN", currency === "NGN" ? exponent : void 0)];
34
- }
35
27
  if (currency && prices[currency] !== void 0) {
36
28
  return [formatPlanPrice(prices[currency], currency, exponent)];
37
29
  }
@@ -50,7 +42,6 @@ export {
50
42
  byCurrency,
51
43
  compareMinor,
52
44
  formatPlanPrice,
53
- pickPlanPrice,
54
45
  planPriceLines,
55
46
  planPriceText
56
47
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plan-price.ts"],"sourcesContent":["import { formatMinor } from \"./money-minor\";\nimport type { Prices } from \"./admin-pricing-api\";\n\n/**\n * Plan prices as people read them. Amounts stay minor-unit strings and are\n * split at the currency's exponent as text: never divided by 100, never\n * added across currencies.\n */\n\nconst fixed = (currency: string, exponent: number) =>\n new Intl.NumberFormat(\"en\", {\n style: \"currency\",\n currency,\n currencyDisplay: \"narrowSymbol\",\n minimumFractionDigits: exponent,\n maximumFractionDigits: exponent,\n });\n\n/**\n * \"₦15,000\" from \"1500000\" NGN. With the exponent the API sent, the digits\n * are split there; without one, Intl's own exponent for the code is used.\n */\nexport function formatPlanPrice(\n minor: string,\n currency: string,\n exponent?: number | null\n): string {\n if (exponent === undefined || exponent === null) {\n return formatMinor(minor, currency, { trimZeros: true });\n }\n\n const digits = minor.replace(/^0+(?=\\d)/, \"\").padStart(exponent + 1, \"0\");\n const whole = digits.slice(0, digits.length - exponent);\n const fraction = digits.slice(digits.length - exponent);\n const decimal = fraction ? `${whole}.${fraction}` : whole;\n let text: string;\n\n try {\n text = fixed(currency, exponent).format(decimal as unknown as number);\n } catch {\n text = `${currency} ${decimal}`;\n }\n\n return text.replace(/[.,]0+(?=\\D*$)/, \"\");\n}\n\n/** NGN first, then the rest by code. */\nexport const byCurrency = (a: string, b: string) =>\n a === \"NGN\" ? -1 : b === \"NGN\" ? 1 : a.localeCompare(b);\n\n/**\n * The one price to show for a plan whose prices are plain numbers in major\n * units: naira when the plan has a naira price, then the preferred currency\n * if it has one, then the first other currency by code. Never a conversion,\n * and the currency comes back with the amount so it is never shown under\n * another currency's sign.\n */\nexport function pickPlanPrice(\n prices: Record<string, number> | null | undefined,\n preferred?: string | null\n): { currency: string; amount: number } | null {\n if (!prices) return null;\n\n const code =\n prices[\"NGN\"] !== undefined\n ? \"NGN\"\n : preferred && prices[preferred] !== undefined\n ? preferred\n : Object.keys(prices).sort((a, b) => a.localeCompare(b))[0];\n\n return code === undefined ? null : { currency: code, amount: prices[code] as number };\n}\n\n/**\n * The price lines to show. Asked for one currency (the single price a\n * merchant sees): a plan with a naira price shows just that, else the\n * currency asked for if the plan has it, else every currency it has. Asked\n * for none, every currency it has, one line each, naira first. The exponent\n * only applies to the currency asked for.\n */\nexport function planPriceLines(\n prices: Prices,\n currency?: string | null,\n exponent?: number | null\n): string[] {\n if (currency && prices[\"NGN\"] !== undefined) {\n return [formatPlanPrice(prices[\"NGN\"], \"NGN\", currency === \"NGN\" ? exponent : undefined)];\n }\n\n if (currency && prices[currency] !== undefined) {\n return [formatPlanPrice(prices[currency], currency, exponent)];\n }\n\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) => formatPlanPrice(prices[code], code));\n}\n\n/** \"₦15,000 a month\", or \"₦15,000 a month · $10 a month\" across currencies. */\nexport function planPriceText(\n prices: Prices,\n currency?: string | null,\n exponent?: number | null\n): string {\n const lines = planPriceLines(prices, currency, exponent);\n\n return lines.length ? lines.map((l) => `${l} a month`).join(\" · \") : \"No price\";\n}\n\n/**\n * Orders two minor-unit amounts as bigints: negative when `a` is cheaper.\n * A missing amount sorts after any amount.\n */\nexport function compareMinor(\n a: string | null | undefined,\n b: string | null | undefined\n): number {\n if (a == null || b == null) return a == null ? (b == null ? 0 : 1) : -1;\n\n const diff = BigInt(a) - BigInt(b);\n\n return diff < BigInt(0) ? -1 : diff > BigInt(0) ? 1 : 0;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAS5B,MAAM,QAAQ,CAAC,UAAkB,aAC/B,IAAI,KAAK,aAAa,MAAM;AAAA,EAC1B,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,uBAAuB;AAAA,EACvB,uBAAuB;AACzB,CAAC;AAMI,SAAS,gBACd,OACA,UACA,UACQ;AACR,MAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,WAAO,YAAY,OAAO,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,EACzD;AAEA,QAAM,SAAS,MAAM,QAAQ,aAAa,EAAE,EAAE,SAAS,WAAW,GAAG,GAAG;AACxE,QAAM,QAAQ,OAAO,MAAM,GAAG,OAAO,SAAS,QAAQ;AACtD,QAAM,WAAW,OAAO,MAAM,OAAO,SAAS,QAAQ;AACtD,QAAM,UAAU,WAAW,GAAG,KAAK,IAAI,QAAQ,KAAK;AACpD,MAAI;AAEJ,MAAI;AACF,WAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,OAA4B;AAAA,EACtE,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,OAAO;AAAA,EAC/B;AAEA,SAAO,KAAK,QAAQ,kBAAkB,EAAE;AAC1C;AAGO,MAAM,aAAa,CAAC,GAAW,MACpC,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI,EAAE,cAAc,CAAC;AASjD,SAAS,cACd,QACA,WAC6C;AAC7C,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,OACJ,OAAO,KAAK,MAAM,SACd,QACA,aAAa,OAAO,SAAS,MAAM,SACjC,YACA,OAAO,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;AAEhE,SAAO,SAAS,SAAY,OAAO,EAAE,UAAU,MAAM,QAAQ,OAAO,IAAI,EAAY;AACtF;AASO,SAAS,eACd,QACA,UACA,UACU;AACV,MAAI,YAAY,OAAO,KAAK,MAAM,QAAW;AAC3C,WAAO,CAAC,gBAAgB,OAAO,KAAK,GAAG,OAAO,aAAa,QAAQ,WAAW,MAAS,CAAC;AAAA,EAC1F;AAEA,MAAI,YAAY,OAAO,QAAQ,MAAM,QAAW;AAC9C,WAAO,CAAC,gBAAgB,OAAO,QAAQ,GAAG,UAAU,QAAQ,CAAC;AAAA,EAC/D;AAEA,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,UAAU,EACf,IAAI,CAAC,SAAS,gBAAgB,OAAO,IAAI,GAAG,IAAI,CAAC;AACtD;AAGO,SAAS,cACd,QACA,UACA,UACQ;AACR,QAAM,QAAQ,eAAe,QAAQ,UAAU,QAAQ;AAEvD,SAAO,MAAM,SAAS,MAAM,IAAI,CAAC,MAAM,GAAG,CAAC,UAAU,EAAE,KAAK,QAAK,IAAI;AACvE;AAMO,SAAS,aACd,GACA,GACQ;AACR,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO,KAAK,OAAQ,KAAK,OAAO,IAAI,IAAK;AAErE,QAAM,OAAO,OAAO,CAAC,IAAI,OAAO,CAAC;AAEjC,SAAO,OAAO,OAAO,CAAC,IAAI,KAAK,OAAO,OAAO,CAAC,IAAI,IAAI;AACxD;","names":[]}
1
+ {"version":3,"sources":["../src/plan-price.ts"],"sourcesContent":["import { formatMinor } from \"./money-minor\";\nimport type { Prices } from \"./admin-pricing-api\";\n\n/**\n * Plan prices as people read them. Amounts stay minor-unit strings and are\n * split at the currency's exponent as text: never divided by 100, never\n * added across currencies.\n */\n\nconst fixed = (currency: string, exponent: number) =>\n new Intl.NumberFormat(\"en\", {\n style: \"currency\",\n currency,\n currencyDisplay: \"narrowSymbol\",\n minimumFractionDigits: exponent,\n maximumFractionDigits: exponent,\n });\n\n/**\n * \"₦15,000\" from \"1500000\" NGN. With the exponent the API sent, the digits\n * are split there; without one, Intl's own exponent for the code is used.\n */\nexport function formatPlanPrice(\n minor: string,\n currency: string,\n exponent?: number | null\n): string {\n if (exponent === undefined || exponent === null) {\n return formatMinor(minor, currency, { trimZeros: true });\n }\n\n const digits = minor.replace(/^0+(?=\\d)/, \"\").padStart(exponent + 1, \"0\");\n const whole = digits.slice(0, digits.length - exponent);\n const fraction = digits.slice(digits.length - exponent);\n const decimal = fraction ? `${whole}.${fraction}` : whole;\n let text: string;\n\n try {\n text = fixed(currency, exponent).format(decimal as unknown as number);\n } catch {\n text = `${currency} ${decimal}`;\n }\n\n return text.replace(/[.,]0+(?=\\D*$)/, \"\");\n}\n\n/** NGN first, then the rest by code. */\nexport const byCurrency = (a: string, b: string) =>\n a === \"NGN\" ? -1 : b === \"NGN\" ? 1 : a.localeCompare(b);\n\n/**\n * The price lines to show: the one currency asked for when the plan has a\n * price in it, otherwise every currency it has, one line each.\n */\nexport function planPriceLines(\n prices: Prices,\n currency?: string | null,\n exponent?: number | null\n): string[] {\n if (currency && prices[currency] !== undefined) {\n return [formatPlanPrice(prices[currency], currency, exponent)];\n }\n\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) => formatPlanPrice(prices[code], code));\n}\n\n/** \"₦15,000 a month\", or \"₦15,000 a month · $10 a month\" across currencies. */\nexport function planPriceText(\n prices: Prices,\n currency?: string | null,\n exponent?: number | null\n): string {\n const lines = planPriceLines(prices, currency, exponent);\n\n return lines.length ? lines.map((l) => `${l} a month`).join(\" · \") : \"No price\";\n}\n\n/**\n * Orders two minor-unit amounts as bigints: negative when `a` is cheaper.\n * A missing amount sorts after any amount.\n */\nexport function compareMinor(\n a: string | null | undefined,\n b: string | null | undefined\n): number {\n if (a == null || b == null) return a == null ? (b == null ? 0 : 1) : -1;\n\n const diff = BigInt(a) - BigInt(b);\n\n return diff < BigInt(0) ? -1 : diff > BigInt(0) ? 1 : 0;\n}\n"],"mappings":"AAAA,SAAS,mBAAmB;AAS5B,MAAM,QAAQ,CAAC,UAAkB,aAC/B,IAAI,KAAK,aAAa,MAAM;AAAA,EAC1B,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,uBAAuB;AAAA,EACvB,uBAAuB;AACzB,CAAC;AAMI,SAAS,gBACd,OACA,UACA,UACQ;AACR,MAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,WAAO,YAAY,OAAO,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,EACzD;AAEA,QAAM,SAAS,MAAM,QAAQ,aAAa,EAAE,EAAE,SAAS,WAAW,GAAG,GAAG;AACxE,QAAM,QAAQ,OAAO,MAAM,GAAG,OAAO,SAAS,QAAQ;AACtD,QAAM,WAAW,OAAO,MAAM,OAAO,SAAS,QAAQ;AACtD,QAAM,UAAU,WAAW,GAAG,KAAK,IAAI,QAAQ,KAAK;AACpD,MAAI;AAEJ,MAAI;AACF,WAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,OAA4B;AAAA,EACtE,QAAQ;AACN,WAAO,GAAG,QAAQ,IAAI,OAAO;AAAA,EAC/B;AAEA,SAAO,KAAK,QAAQ,kBAAkB,EAAE;AAC1C;AAGO,MAAM,aAAa,CAAC,GAAW,MACpC,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI,EAAE,cAAc,CAAC;AAMjD,SAAS,eACd,QACA,UACA,UACU;AACV,MAAI,YAAY,OAAO,QAAQ,MAAM,QAAW;AAC9C,WAAO,CAAC,gBAAgB,OAAO,QAAQ,GAAG,UAAU,QAAQ,CAAC;AAAA,EAC/D;AAEA,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,UAAU,EACf,IAAI,CAAC,SAAS,gBAAgB,OAAO,IAAI,GAAG,IAAI,CAAC;AACtD;AAGO,SAAS,cACd,QACA,UACA,UACQ;AACR,QAAM,QAAQ,eAAe,QAAQ,UAAU,QAAQ;AAEvD,SAAO,MAAM,SAAS,MAAM,IAAI,CAAC,MAAM,GAAG,CAAC,UAAU,EAAE,KAAK,QAAK,IAAI;AACvE;AAMO,SAAS,aACd,GACA,GACQ;AACR,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO,KAAK,OAAQ,KAAK,OAAO,IAAI,IAAK;AAErE,QAAM,OAAO,OAAO,CAAC,IAAI,OAAO,CAAC;AAEjC,SAAO,OAAO,OAAO,CAAC,IAAI,KAAK,OAAO,OAAO,CAAC,IAAI,IAAI;AACxD;","names":[]}
@@ -1,6 +1,4 @@
1
1
  import type { ContentBehavior, TemplateConfig } from './template-types';
2
- /** The standalone behaviors this tenant has explicitly turned on. */
3
- export declare function resolveStandaloneBehaviors(tenantSettings: Record<string, unknown> | undefined): ContentBehavior[];
4
2
  export declare function resolveBehaviorsFromIntents(intents: string[]): ContentBehavior[];
5
3
  /**
6
4
  * What a single model grants on its own — used by Settings → Features to show,