@mohasinac/appkit 4.31.0 → 4.32.0

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 (53) hide show
  1. package/dist/_internal/server/features/products/list-public.d.ts +18 -0
  2. package/dist/_internal/server/features/products/list-public.js +27 -9
  3. package/dist/client.d.ts +9 -0
  4. package/dist/client.js +12 -0
  5. package/dist/features/admin/components/AdminAdsView.js +36 -34
  6. package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
  7. package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
  8. package/dist/features/admin/components/AdminProductEditorView.js +78 -18
  9. package/dist/features/admin/components/AdminSectionsView.js +89 -32
  10. package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
  11. package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
  12. package/dist/features/admin/components/AdminUserEditorView.js +65 -82
  13. package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
  14. package/dist/features/admin/constants/admin-page-tabs.js +45 -0
  15. package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
  16. package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
  17. package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
  18. package/dist/features/admin/schemas/admin-user-form.js +96 -7
  19. package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
  20. package/dist/features/layout/PageTabs.d.ts +17 -0
  21. package/dist/features/layout/PageTabs.js +45 -0
  22. package/dist/features/products/components/GroupSettingsPanel.js +62 -16
  23. package/dist/features/search/action-index/tab-entries.d.ts +18 -0
  24. package/dist/features/search/action-index/tab-entries.js +128 -0
  25. package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
  26. package/dist/features/seller/components/SellerOrdersView.js +57 -19
  27. package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
  28. package/dist/features/seller/components/SellerReviewsView.js +54 -19
  29. package/dist/features/seller/constants/store-tabs.d.ts +64 -0
  30. package/dist/features/seller/constants/store-tabs.js +60 -0
  31. package/dist/features/seller/schemas/order-forms.d.ts +33 -0
  32. package/dist/features/seller/schemas/order-forms.js +75 -0
  33. package/dist/features/seller/schemas/review-forms.d.ts +37 -0
  34. package/dist/features/seller/schemas/review-forms.js +82 -0
  35. package/dist/features/shell/index.d.ts +1 -1
  36. package/dist/features/shell/index.js +5 -1
  37. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
  38. package/dist/features/shipments/schemas/validation.d.ts +41 -0
  39. package/dist/features/shipments/schemas/validation.js +49 -0
  40. package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
  41. package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
  42. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +67 -0
  43. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +2 -0
  46. package/dist/next/index.d.ts +8 -0
  47. package/dist/next/index.js +37 -0
  48. package/dist/react/hooks/useTabParam.d.ts +19 -0
  49. package/dist/react/hooks/useTabParam.js +65 -0
  50. package/dist/schemas/registry.d.ts +27 -3
  51. package/dist/styles.css +1 -1
  52. package/dist/tailwind-utilities.css +1 -1
  53. package/package.json +1 -1
@@ -396,6 +396,12 @@ const rawTesterChecklistItems = [
396
396
  href: "/products",
397
397
  cases: [
398
398
  { key: "standard-detail", label: "Standard product detail page loads correctly", href: "/products/product-tester-standard-1" },
399
+ {
400
+ key: "tester-fixtures-hidden-from-the-public",
401
+ label: "SIGNED OUT, no product on /products has an id containing \"tester\" — the sandbox is invisible to the public",
402
+ description: "Fixed 2026-09-01. BEFORE: the public products endpoint had no viewer concept, so an ANONYMOUS caller got 25 tester fixtures in its first 40 rows — stickers-tester-sandbox-1, prizedraw-tester-sandbox-closed, live-tester-sandbox-1 — and they filled the public marketplace. AFTER: open /products in a private window and scan the grid; nothing should be named \"Tester …\". Then sign in AS A TESTER and reload — the sandbox items must come back. Both halves matter: hiding them from everyone would break the tester programme, and a fixture that never reappears is the opposite bug.",
403
+ href: "/products",
404
+ },
399
405
  {
400
406
  key: "seed-images-are-labelled-tiles",
401
407
  label: "Every seeded product image is a readable tile showing the item's own name — NOT a broken-image icon and NOT an empty grey box",
@@ -2053,6 +2059,67 @@ const rawTesterChecklistItems = [
2053
2059
  { key: "shipping-configs-crud", label: "Creating, editing, and listing shipping configs works", href: "/store/shipping-configs" },
2054
2060
  { key: "payout-methods-crud", label: "Adding, editing, and listing payout methods works", href: "/store/payout-methods" },
2055
2061
  { key: "payout-settings-page", label: "Payout settings page saves correctly", href: "/store/payout-settings" },
2062
+ {
2063
+ key: "consolidated-tabs-old-urls",
2064
+ label: "Each absorbed URL still lands on the right TAB, not just the right page",
2065
+ description: "W8 C2 folded six sibling pages into tabs. BEFORE: /store/payout-methods, /store/payout-settings, /store/shipping-configs, /store/print-center, /store/slug, /admin/permissions and /admin/settings/navigation were each their own page with their own sidebar entry. AFTER: each redirects to its host with ?tab= set, and the sidebar entry is gone. Visit all seven old URLs. Each must land on the host page with the CORRECT tab already selected — a landing on the host's FIRST tab is the failure, and it looks like success unless you check which tab is active.",
2066
+ href: "/store/payout-methods",
2067
+ },
2068
+ {
2069
+ key: "consolidated-tabs-url-writeback",
2070
+ label: "Clicking a tab updates ?tab= in the address bar",
2071
+ description: "BEFORE: the settings page read ?tab= once on load and never wrote it back, so copying the URL after clicking a tab shared the DEFAULT tab. AFTER: every tabbed page writes the tab into the URL on click. On /store/payouts click through all three tabs, watch ?tab= change, then copy the address and open it in a new tab — it must reopen on the same panel. Back must leave the page rather than walking back through the tabs.",
2072
+ href: "/store/payouts",
2073
+ },
2074
+ {
2075
+ key: "print-center-selection-deeplink",
2076
+ label: "Print labels for a selection still carries the selection through the redirect",
2077
+ description: "/store/print-center is now a redirect, and it is deep-linked from the orders and products lists as ?type=order&ids=…&autoprint=1. Select two orders on /store/orders, use the print action, and confirm the Print Centre tab opens with those two orders loaded. Arriving at an EMPTY Print Centre is the failure — it looks like a normal empty state, which is why this case names the before.",
2078
+ href: "/store/orders",
2079
+ },
2080
+ {
2081
+ key: "consolidated-tabs-command-palette",
2082
+ label: "The absorbed surfaces are still findable by name in search",
2083
+ description: "Their sidebar entries were deleted, and the action index is derived from the sidebar — so without a deliberate entry they would vanish from search on the day they became easier to reach. Search for \"upi\", \"threshold\", \"barcode\", \"slug\" and \"permissions catalogue\". Each must return a result that opens the right TAB.",
2084
+ href: "/store/payouts",
2085
+ },
2086
+ ],
2087
+ },
2088
+ {
2089
+ pageKey: "sectionised-forms",
2090
+ pageLabel: "Forms — sections, error summary and the mobile bar",
2091
+ href: "/admin/orders",
2092
+ cases: [
2093
+ {
2094
+ key: "form-sections-save-unchanged",
2095
+ label: "Every migrated form still saves exactly the fields it saved before",
2096
+ description: "W8 C3 moved 16 hand-rolled forms onto <SectionForm>. The risk is a field that renders but no longer reaches the payload. On each of these, change EVERY field, save, reload, and confirm each value came back: the admin order editor (status, tracking number, carrier, notes), the admin user editor (role, email verified, display name, phone, tester flags, admin notes, bio, location, website, four social links), WhatsApp settings, the seller order panel, and the ad settings panel. A field that silently reverts is the failure, and nothing errors when it happens.",
2097
+ href: "/admin/orders",
2098
+ },
2099
+ {
2100
+ key: "form-error-summary-jumps",
2101
+ label: "A validation error names its section and jumps to it",
2102
+ description: "BEFORE: an invalid field produced a banner listing the problem with no way to reach it, and on the site-settings page the summary was mounted outside every form context and so displayed NOTHING at all. AFTER: submit a form with a bad value (a negative refund on an order, a non-https third-party ad URL, an AdSense id that is not ca-pub-…) and the summary must list it, tag it with its section, collapse open that section when clicked, and show a count badge on the section header.",
2103
+ href: "/admin/ads",
2104
+ },
2105
+ {
2106
+ key: "form-mobile-action-bar",
2107
+ label: "Save and Cancel appear in the pinned mobile bar, once",
2108
+ description: "On a phone-width viewport, a page-level form publishes its actions into the bottom chrome tier. Open /admin/products/new and confirm exactly ONE Save row is visible — not one in the page and one pinned. Inside a drawer or modal (the admin order editor, the review reply modal) the pinned bar must NOT appear: an overlay owns its own footer, and a viewport-fixed bar renders behind the backdrop.",
2109
+ href: "/admin/products/new",
2110
+ },
2111
+ {
2112
+ key: "form-conditional-fields-drop-values",
2113
+ label: "A field hidden by a condition does not submit a stale value",
2114
+ description: "BEFORE: hiding a control left its last value in the payload. AFTER: on the admin user editor, switch \"Is Tester\" ON, tick \"Can test admin areas\", switch \"Is Tester\" back OFF, save, reload — the admin-test flag must be false. Same shape on a shipment item: enter a price, tick \"for self use\", save; the price must not be stored.",
2115
+ href: "/admin/users",
2116
+ },
2117
+ {
2118
+ key: "site-settings-save-still-writes-everything",
2119
+ label: "Site settings still saves every tab's fields in one Save",
2120
+ description: "Its 20 tabs write ONE document through one payload, and its schema is deliberately passthrough because that payload carries keys the TypeScript interface does not declare. A closed schema here would silently strip them. Change one field on each of three different tabs, press \"Save all changes\" once, reload, and confirm all three persisted — and that fields on tabs you did NOT touch are unchanged.",
2121
+ href: "/admin/site",
2122
+ },
2056
2123
  ],
2057
2124
  },
2058
2125
  {
@@ -12,12 +12,15 @@ const __O = {
12
12
  };
13
13
  const CLS_SECTION_CARD = "border border-[var(--appkit-color-border)] rounded-xl p-[var(--appkit-space-5)]";
14
14
  import { useQuery, useQueryClient } from "@tanstack/react-query";
15
- import { Alert, Badge, Button, Div, Form, FormActions, Heading, Input, Row, Section, Span, Stack, Text, Toggle, useToast } from "../../../ui";
15
+ import { Alert, Badge, Button, Div, Heading, Input, Row, Section, Span, Stack, Text, Toggle, useToast } from "../../../ui";
16
16
  import { apiClient } from "../../../http";
17
17
  import { WHATSAPP_SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
18
18
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
19
19
  import { buildPurchaseAnnouncementMessage } from "../helpers/whatsapp";
20
- import { whatsappSettingsSchema } from "../../admin/schemas/admin-user-form";
20
+ import { whatsappConnectionSchema, } from "../../admin/schemas/admin-user-form";
21
+ import { SectionForm, useSectionFormNav, buildSectionsFromSchema, visibleValues, } from "../../shell";
22
+ import { useFormShellState, FormShellContext } from "../../../ui/forms";
23
+ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
21
24
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
22
25
  // ---------------------------------------------------------------------------
23
26
  // Sub-components
@@ -101,20 +104,40 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
101
104
  });
102
105
  const cfg = data ?? null;
103
106
  // Connection form state
104
- const [phoneNumber, setPhoneNumber] = React.useState("");
105
- const [wabaId, setWabaId] = React.useState("");
106
- const [catalogId, setCatalogId] = React.useState("");
107
- const [accessToken, setAccessToken] = React.useState("");
107
+ const [draft, setDraft] = React.useState({
108
+ phoneNumber: "",
109
+ wabaId: "",
110
+ catalogId: "",
111
+ accessToken: "",
112
+ });
108
113
  const [syncEnabled, setSyncEnabled] = React.useState(false);
109
114
  React.useEffect(() => {
110
115
  if (!cfg)
111
116
  return;
112
- setPhoneNumber(cfg.phoneNumber ?? "");
113
- setWabaId(cfg.wabaId ?? "");
114
- setCatalogId(cfg.catalogId ?? "");
115
- setAccessToken(cfg.accessToken ?? "");
117
+ setDraft({
118
+ phoneNumber: cfg.phoneNumber ?? "",
119
+ wabaId: cfg.wabaId ?? "",
120
+ catalogId: cfg.catalogId ?? "",
121
+ accessToken: cfg.accessToken ?? "",
122
+ });
116
123
  setSyncEnabled(cfg.catalogSyncEnabled ?? false);
117
124
  }, [cfg]);
125
+ /*
126
+ * The access token needs a renderer: it is a `text` field to the schema, and
127
+ * the control is a masked input with a reveal that must stay suppressed while
128
+ * the field still holds the server's sentinel.
129
+ */
130
+ const sections = React.useMemo(() => buildSectionsFromSchema(whatsappConnectionSchema, {
131
+ renderers: {
132
+ accessToken: ({ values, onChange }) => (_jsx(MaskedTokenInput, { label: "System User Access Token", value: values.accessToken ?? "", onChange: (v) => onChange({ accessToken: v }), placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived token with WhatsApp Business permissions" })),
133
+ },
134
+ }), []);
135
+ const nav = useSectionFormNav(sections, draft, { scope: "store:whatsapp" });
136
+ const form = useFormShellState(whatsappConnectionSchema, {
137
+ sections: nav.sectionMeta,
138
+ onGoToSection: nav.goToSection,
139
+ fieldToSectionIndex: nav.fieldToSectionIndex,
140
+ });
118
141
  /*
119
142
  * The CONNECTION form. Credentials only.
120
143
  *
@@ -131,16 +154,17 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
131
154
  const saveMutation = useApiMutation({
132
155
  errorMessage: "Failed to save settings.",
133
156
  mutationFn: async () => {
157
+ const v = visibleValues(whatsappConnectionSchema, draft);
134
158
  const payload = {};
135
- if (phoneNumber)
136
- payload.phoneNumber = phoneNumber;
137
- if (wabaId)
138
- payload.wabaId = wabaId;
139
- if (catalogId)
140
- payload.catalogId = catalogId;
159
+ if (v.phoneNumber)
160
+ payload.phoneNumber = v.phoneNumber;
161
+ if (v.wabaId)
162
+ payload.wabaId = v.wabaId;
163
+ if (v.catalogId)
164
+ payload.catalogId = v.catalogId;
141
165
  // Only send accessToken if user has typed a new value (not the masked placeholder)
142
- if (accessToken && accessToken !== MASKED_TOKEN)
143
- payload.accessToken = accessToken;
166
+ if (v.accessToken && v.accessToken !== MASKED_TOKEN)
167
+ payload.accessToken = v.accessToken;
144
168
  return apiClient.put(WHATSAPP_SELLER_ENDPOINTS.SETTINGS, payload);
145
169
  },
146
170
  onSuccess: () => {
@@ -224,10 +248,17 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
224
248
  ? cfg?.connected === true
225
249
  : Boolean(cfg?.[step.checkKey]);
226
250
  return (_jsxs(Row, { gap: "sm", align: "start", children: [_jsx(Row, { textWeight: "bold", textSize: "xs", className: `flex-shrink-0 w-6 h-6 mt-0.5 ${done ? "bg-[var(--appkit-color-success)] text-white" : "bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] dark:text-[var(--appkit-color-text-faint)]"}`, align: "center", justify: "center", rounded: "full", children: done ? "✓" : step.n }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: step.title }), _jsx(Text, { className: "mt-0.5", color: "muted", size: "xs", children: step.body })] })] }, step.n));
227
- }) })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsxs(Row, { justify: "between", align: "center", className: "mb-4", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "primary", children: "Connection" }), isLoading ? null : cfg?.connected ? (_jsx(Badge, { variant: "success", children: "Connected" })) : (_jsx(Badge, { variant: "default", children: "Not configured" }))] }), _jsxs(Form, { schema: whatsappSettingsSchema, onSubmit: (e) => {
228
- e.preventDefault();
229
- saveMutation.mutate();
230
- }, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "WhatsApp Business Phone Number", value: phoneNumber, onChange: (e) => setPhoneNumber(e.target.value), placeholder: "919876543210", helperText: "Digits only, include country code (e.g. 91 for India)" }), _jsx(Input, { label: "WABA ID (WhatsApp Business Account ID)", value: wabaId, onChange: (e) => setWabaId(e.target.value), placeholder: "123456789012345", helperText: "From Meta Business Manager \u2192 WhatsApp Accounts" }), _jsx(Input, { label: "Catalog ID", value: catalogId, onChange: (e) => setCatalogId(e.target.value), placeholder: "987654321098765", helperText: "From Meta Commerce Manager \u2192 Catalogs" }), _jsx(MaskedTokenInput, { label: "System User Access Token", value: accessToken, onChange: setAccessToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived token with WhatsApp Business permissions" }), _jsx(FormActions, { align: "right", children: _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: saveMutation.isPending, children: saveMutation.isPending ? "Saving…" : ACTIONS.STORE["whatsapp-connect"].label }) })] })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsx(Heading, { level: 2, className: "mb-4", color: "primary", size: "sm", weight: "semibold", children: "Catalog Sync" }), _jsxs(Row, { justify: "between", align: "center", className: "mb-4", children: [_jsxs(Div, { children: [_jsx(Text, { size: "sm", color: "muted", children: "Enable catalog sync" }), _jsx(Text, { size: "xs", color: "muted", children: "When enabled, your published standard products can be synced to WhatsApp." })] }), _jsx(Toggle, { checked: syncEnabled, onChange: handleSyncToggle, disabled: !cfg?.connected || syncToggleMutation.isPending })] }), cfg?.lastCatalogSyncAt && (_jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", align: "center", className: "mb-3", children: [_jsxs(Text, { children: ["Last sync: ", new Date(cfg.lastCatalogSyncAt).toLocaleString("en-IN")] }), cfg.lastSyncCount !== undefined && _jsxs(Text, { children: ["\u00B7 ", cfg.lastSyncCount, " products"] }), cfg.lastSyncStatus && (_jsx(Badge, { variant: STATUS_VARIANT[cfg.lastSyncStatus] ?? "default", children: cfg.lastSyncStatus }))] })), _jsx(Text, { className: "mb-3", color: "faint", size: "xs", children: "Only published standard products are synced. Auctions and pre-orders are excluded. Batches of up to 50 products per call." }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { onClick: () => syncMutation.mutate(), isLoading: syncMutation.isPending, disabled: !cfg?.connected || !syncEnabled || syncMutation.isPending, variant: "secondary", children: syncMutation.isPending ? "Syncing…" : ACTIONS.STORE["whatsapp-catalog-sync"].label }), _jsx(Button, { onClick: () => void startImport(), isLoading: importing, disabled: !cfg?.connected || !syncEnabled || importing, variant: "secondary", children: importing ? "Importing…" : ACTIONS.STORE["whatsapp-catalog-import"].label })] }), _jsxs(Text, { className: "mt-3", color: "faint", size: "xs", children: [_jsx(Span, { weight: "bold", children: "Push" }), " sends your published standard products to WhatsApp.", " ", _jsx(Span, { weight: "bold", children: "Import" }), " creates draft products from your WhatsApp catalog. Products are matched by slug in the description field."] })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "sm", weight: "semibold", children: "Purchase Announcement Preview" }), _jsx(Text, { className: "mb-3", color: "muted", size: "xs", children: "This message is sent automatically to your phone and the platform admin when a new order is placed." }), _jsx(Div, { textSize: "sm", className: "font-mono", color: "primary", surface: "muted", padding: "inline", rounded: "lg", children: buildPurchaseAnnouncementMessage({
251
+ }) })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsxs(Row, { justify: "between", align: "center", className: "mb-4", children: [_jsx(Heading, { level: 2, size: "sm", weight: "semibold", color: "primary", children: "Connection" }), isLoading ? null : cfg?.connected ? (_jsx(Badge, { variant: "success", children: "Connected" })) : (_jsx(Badge, { variant: "default", children: "Not configured" }))] }), _jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
252
+ // SectionForm scrolls to the first bad section and then submits
253
+ // regardless, so the guard lives here.
254
+ form.clearErrors();
255
+ const parsed = whatsappConnectionSchema.safeParse(visibleValues(whatsappConnectionSchema, draft));
256
+ if (!parsed.success) {
257
+ applyZodIssues(parsed.error.issues, form.setFieldError);
258
+ return;
259
+ }
260
+ saveMutation.mutate();
261
+ }, onValidationChange: () => form.validate(draft), schema: whatsappConnectionSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: ACTIONS.STORE["whatsapp-connect"].label, isLoading: saveMutation.isPending })] })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsx(Heading, { level: 2, className: "mb-4", color: "primary", size: "sm", weight: "semibold", children: "Catalog Sync" }), _jsxs(Row, { justify: "between", align: "center", className: "mb-4", children: [_jsxs(Div, { children: [_jsx(Text, { size: "sm", color: "muted", children: "Enable catalog sync" }), _jsx(Text, { size: "xs", color: "muted", children: "When enabled, your published standard products can be synced to WhatsApp." })] }), _jsx(Toggle, { checked: syncEnabled, onChange: handleSyncToggle, disabled: !cfg?.connected || syncToggleMutation.isPending })] }), cfg?.lastCatalogSyncAt && (_jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", align: "center", className: "mb-3", children: [_jsxs(Text, { children: ["Last sync: ", new Date(cfg.lastCatalogSyncAt).toLocaleString("en-IN")] }), cfg.lastSyncCount !== undefined && _jsxs(Text, { children: ["\u00B7 ", cfg.lastSyncCount, " products"] }), cfg.lastSyncStatus && (_jsx(Badge, { variant: STATUS_VARIANT[cfg.lastSyncStatus] ?? "default", children: cfg.lastSyncStatus }))] })), _jsx(Text, { className: "mb-3", color: "faint", size: "xs", children: "Only published standard products are synced. Auctions and pre-orders are excluded. Batches of up to 50 products per call." }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { onClick: () => syncMutation.mutate(), isLoading: syncMutation.isPending, disabled: !cfg?.connected || !syncEnabled || syncMutation.isPending, variant: "secondary", children: syncMutation.isPending ? "Syncing…" : ACTIONS.STORE["whatsapp-catalog-sync"].label }), _jsx(Button, { onClick: () => void startImport(), isLoading: importing, disabled: !cfg?.connected || !syncEnabled || importing, variant: "secondary", children: importing ? "Importing…" : ACTIONS.STORE["whatsapp-catalog-import"].label })] }), _jsxs(Text, { className: "mt-3", color: "faint", size: "xs", children: [_jsx(Span, { weight: "bold", children: "Push" }), " sends your published standard products to WhatsApp.", " ", _jsx(Span, { weight: "bold", children: "Import" }), " creates draft products from your WhatsApp catalog. Products are matched by slug in the description field."] })] }), _jsxs(Section, { className: CLS_SECTION_CARD, children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "sm", weight: "semibold", children: "Purchase Announcement Preview" }), _jsx(Text, { className: "mb-3", color: "muted", size: "xs", children: "This message is sent automatically to your phone and the platform admin when a new order is placed." }), _jsx(Div, { textSize: "sm", className: "font-mono", color: "primary", surface: "muted", padding: "inline", rounded: "lg", children: buildPurchaseAnnouncementMessage({
231
262
  buyerName: "Ravi K.",
232
263
  firstItemName: "Charizard PSA 9",
233
264
  additionalItemCount: 2,
package/dist/index.d.ts CHANGED
@@ -3220,5 +3220,9 @@ export { SITE_SETTINGS_ENTRIES, deriveSettingsEntries, } from "./features/search
3220
3220
  export type { SettingsIndexSource } from "./features/search/action-index/settings-entries";
3221
3221
  export { SITE_SETTINGS_TABS, SITE_SETTINGS_TAB_IDS, isSiteSettingsTabId, } from "./features/admin/constants/site-settings-tabs";
3222
3222
  export type { SiteSettingsTab, SiteSettingsTabId } from "./features/admin/constants/site-settings-tabs";
3223
+ export { PAYOUTS_TABS, isPayoutsTabId, SHIPPING_TABS, isShippingTabId, FULFILLMENT_TABS, isFulfillmentTabId, STOREFRONT_TABS, isStorefrontTabId, } from "./features/seller/constants/store-tabs";
3224
+ export type { StoreDashboardTab, PayoutsTabId, ShippingTabId, FulfillmentTabId, StorefrontTabId, } from "./features/seller/constants/store-tabs";
3225
+ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
3226
+ export type { AdminPageTab, RolesTabId, AdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
3223
3227
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
3224
3228
  export type { ActionIndexValidationContext, ActionIndexValidationIssue, } from "./features/search/action-index/validate-entry";
package/dist/index.js CHANGED
@@ -5626,4 +5626,6 @@ export { deriveNavEntries, deriveQuickActionEntries, buildActionIndexBase, } fro
5626
5626
  export { matchesNavQuery } from "./_internal/shared/features/layout/matchesNavQuery";
5627
5627
  export { SITE_SETTINGS_ENTRIES, deriveSettingsEntries, } from "./features/search/action-index/settings-entries";
5628
5628
  export { SITE_SETTINGS_TABS, SITE_SETTINGS_TAB_IDS, isSiteSettingsTabId, } from "./features/admin/constants/site-settings-tabs";
5629
+ export { PAYOUTS_TABS, isPayoutsTabId, SHIPPING_TABS, isShippingTabId, FULFILLMENT_TABS, isFulfillmentTabId, STOREFRONT_TABS, isStorefrontTabId, } from "./features/seller/constants/store-tabs";
5630
+ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
5629
5631
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
@@ -32,3 +32,11 @@ export { UnauthorizedView } from "./components/UnauthorizedView";
32
32
  export type { UnauthorizedViewProps } from "./components/UnauthorizedView";
33
33
  export { ErrorBoundary } from "./ErrorBoundary";
34
34
  export type { ErrorBoundaryProps } from "./ErrorBoundary";
35
+ export { navItemId } from "../_internal/shared/features/layout/types";
36
+ export { matchesNavQuery } from "../_internal/shared/features/layout/matchesNavQuery";
37
+ export { buildActionIndexBase, deriveNavEntries, deriveQuickActionEntries, } from "../features/search/action-index/derive";
38
+ export { deriveSettingsEntries } from "../features/search/action-index/settings-entries";
39
+ export { deriveConsolidatedTabEntries, CONSOLIDATED_TAB_SURFACES, } from "../features/search/action-index/tab-entries";
40
+ export type { ConsolidatedTabSurface, ConsolidatedTabHrefs, } from "../features/search/action-index/tab-entries";
41
+ export { mergeActionIndex, projectActionIndexForViewer, } from "../features/search/action-index/types";
42
+ export type { ActionIndexEntry, ActionIndexControl, ActionIndexOverride, } from "../features/search/action-index/types";
@@ -27,3 +27,40 @@ export { ErrorView } from "./components/ErrorView";
27
27
  export { NotFoundView } from "./components/NotFoundView";
28
28
  export { UnauthorizedView } from "./components/UnauthorizedView";
29
29
  export { ErrorBoundary } from "./ErrorBoundary";
30
+ /*
31
+ * Pure, isomorphic nav helpers — deliberately re-exported HERE as well as from
32
+ * the main and client entries.
33
+ *
34
+ * 🛑 `src/constants/navigation.tsx` CALLS `navItemId()` at module scope and is
35
+ * imported by BOTH server code (the locale and store layouts, the action index)
36
+ * and client code (every sidebar). That leaves it nowhere to import from:
37
+ *
38
+ * - `@mohasinac/appkit/client` is `"use client"`, so on the server the binding
39
+ * is a client-reference proxy and calling it throws (Root Cause #76). It did
40
+ * — "Attempted to call navItemId() from the server" failed the Vercel build.
41
+ * - the bare `@mohasinac/appkit` entry then trips
42
+ * `audit-client-server-only-leak` on all 57 client components that reach
43
+ * navigation.tsx transitively (Root Cause #6).
44
+ *
45
+ * The two audits pull in opposite directions, and both are right. `./next` is
46
+ * the third door: not `"use client"`, and not a path to firebase-admin — so it
47
+ * satisfies both. This is the same reasoning that keeps `/ui` and `/tokens`
48
+ * separate entries.
49
+ */
50
+ export { navItemId } from "../_internal/shared/features/layout/types";
51
+ export { matchesNavQuery } from "../_internal/shared/features/layout/matchesNavQuery";
52
+ /*
53
+ * The action-index derivation — same reasoning as the nav helpers above.
54
+ *
55
+ * `src/constants/action-index.ts` calls `buildActionIndexBase()` at module
56
+ * scope and is imported by `GET /api/action-index` (a server route) as well as
57
+ * by `LayoutShellClient` (a client component). Neither the `"use client"` entry
58
+ * nor the bare one works for both; `./next` does.
59
+ *
60
+ * Found by `audit-client-entry-in-server`'s R2 the first time it ran — the same
61
+ * defect as navItemId, one file over, and the next build failure in line.
62
+ */
63
+ export { buildActionIndexBase, deriveNavEntries, deriveQuickActionEntries, } from "../features/search/action-index/derive";
64
+ export { deriveSettingsEntries } from "../features/search/action-index/settings-entries";
65
+ export { deriveConsolidatedTabEntries, CONSOLIDATED_TAB_SURFACES, } from "../features/search/action-index/tab-entries";
66
+ export { mergeActionIndex, projectActionIndexForViewer, } from "../features/search/action-index/types";
@@ -0,0 +1,19 @@
1
+ export interface UseTabParamOptions {
2
+ /**
3
+ * A locale-aware router, when the caller has one. Without it the plain
4
+ * `next/navigation` router drops the locale prefix on replace.
5
+ */
6
+ router?: {
7
+ replace: (url: string) => void;
8
+ };
9
+ pathname?: string;
10
+ }
11
+ /**
12
+ * The active tab, kept in `?tab=`.
13
+ *
14
+ * `isValid` is the page's own type guard — `isSiteSettingsTabId`,
15
+ * `isPayoutsTabId`, and so on — so an unknown value in the URL is IGNORED
16
+ * rather than written into state. That is what makes a stale bookmark open the
17
+ * default tab instead of rendering an empty panel.
18
+ */
19
+ export declare function useTabParam<T extends string>(isValid: (value: string | null | undefined) => value is T, fallback: T, options?: UseTabParamOptions): [T, (next: string) => void];
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ /*
3
+ * WHY: `Tabs` has no URL sync of its own, so every tabbed page has had to
4
+ * hand-roll one. `AdminSiteSettingsView` read `?tab=` once, in a
5
+ * `useState` initialiser guarded on `typeof window` — and never wrote it
6
+ * back, so the URL went stale the moment anyone clicked a tab. A deep
7
+ * link into a tab worked; sharing the page you were looking at did not.
8
+ *
9
+ * W8's tab consolidations add five more tabbed pages, each of which needs
10
+ * the absorbed page's `?tab=` deep link to keep working, which is well
11
+ * past the Rule of Three.
12
+ *
13
+ * WHAT: Read `?tab=` (validated against the page's own union), and write it
14
+ * back on every change.
15
+ *
16
+ * ## Why `router.replace` and not `push`
17
+ *
18
+ * A tab is a view of one page, not a new destination. Pushing would make Back
19
+ * walk the user through every tab they glanced at before leaving the page,
20
+ * which is the behaviour `useUrlTable` already rejected for filters.
21
+ *
22
+ * ## Why not `useUrlTable().set(TABLE_KEYS.TAB, …)`
23
+ *
24
+ * That would work, and it also resets `page` to 1 — correct for a filter
25
+ * change, meaningless here, and it puts a `page=1` in the URL of a settings
26
+ * page that has no pagination.
27
+ *
28
+ * EXPORTS: useTabParam
29
+ *
30
+ * @tag domain:shared
31
+ * @tag layer:hook
32
+ * @tag pattern:none
33
+ * @tag access:client
34
+ * @tag consumers:AdminSiteSettingsView,store payouts/shipping/fulfillment/storefront,admin roles/settings
35
+ * @tag sideEffects:router
36
+ */
37
+ import { useRouter, usePathname, useSearchParams } from "next/navigation";
38
+ import { useCallback } from "react";
39
+ import { TABLE_KEYS } from "../../constants/table-keys";
40
+ /**
41
+ * The active tab, kept in `?tab=`.
42
+ *
43
+ * `isValid` is the page's own type guard — `isSiteSettingsTabId`,
44
+ * `isPayoutsTabId`, and so on — so an unknown value in the URL is IGNORED
45
+ * rather than written into state. That is what makes a stale bookmark open the
46
+ * default tab instead of rendering an empty panel.
47
+ */
48
+ export function useTabParam(isValid, fallback, options) {
49
+ const nativeRouter = useRouter();
50
+ const nativePathname = usePathname();
51
+ const searchParams = useSearchParams();
52
+ const router = options?.router ?? nativeRouter;
53
+ const pathname = options?.pathname ?? nativePathname;
54
+ const requested = searchParams.get(TABLE_KEYS.TAB);
55
+ const active = isValid(requested) ? requested : fallback;
56
+ const setTab = useCallback((next) => {
57
+ if (!isValid(next))
58
+ return;
59
+ const params = new URLSearchParams(searchParams.toString());
60
+ params.set(TABLE_KEYS.TAB, next);
61
+ const query = params.toString();
62
+ router.replace(query ? `${pathname}?${query}` : pathname);
63
+ }, [isValid, pathname, router, searchParams]);
64
+ return [active, setTab];
65
+ }
@@ -4874,26 +4874,50 @@ export declare const SCHEMAS: {
4874
4874
  carrier?: string | undefined;
4875
4875
  }>;
4876
4876
  readonly adminUser: import("zod").ZodObject<{
4877
+ role: import("zod").ZodOptional<import("zod").ZodEnum<["user", "seller", "admin", "moderator"]>>;
4878
+ emailVerified: import("zod").ZodOptional<import("zod").ZodBoolean>;
4877
4879
  displayName: import("zod").ZodOptional<import("zod").ZodString>;
4878
4880
  phoneNumber: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4879
4881
  isTester: import("zod").ZodOptional<import("zod").ZodBoolean>;
4880
4882
  canTestAdmin: import("zod").ZodOptional<import("zod").ZodBoolean>;
4881
4883
  adminNotes: import("zod").ZodOptional<import("zod").ZodString>;
4882
- publicProfile: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("./types").FirestoreValue, import("zod").ZodTypeDef, import("./types").FirestoreValue>>>;
4884
+ bio: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4885
+ location: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4886
+ website: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4887
+ twitter: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4888
+ instagram: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4889
+ facebook: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4890
+ linkedin: import("zod").ZodUnion<[import("zod").ZodOptional<import("zod").ZodString>, import("zod").ZodLiteral<"">]>;
4883
4891
  }, "strip", import("zod").ZodTypeAny, {
4892
+ role?: "user" | "seller" | "admin" | "moderator" | undefined;
4884
4893
  displayName?: string | undefined;
4894
+ location?: string | undefined;
4885
4895
  adminNotes?: string | undefined;
4896
+ bio?: string | undefined;
4886
4897
  phoneNumber?: string | undefined;
4898
+ emailVerified?: boolean | undefined;
4899
+ website?: string | undefined;
4887
4900
  isTester?: boolean | undefined;
4888
4901
  canTestAdmin?: boolean | undefined;
4889
- publicProfile?: Record<string, import("./types").FirestoreValue> | undefined;
4902
+ instagram?: string | undefined;
4903
+ facebook?: string | undefined;
4904
+ twitter?: string | undefined;
4905
+ linkedin?: string | undefined;
4890
4906
  }, {
4907
+ role?: "user" | "seller" | "admin" | "moderator" | undefined;
4891
4908
  displayName?: string | undefined;
4909
+ location?: string | undefined;
4892
4910
  adminNotes?: string | undefined;
4911
+ bio?: string | undefined;
4893
4912
  phoneNumber?: string | undefined;
4913
+ emailVerified?: boolean | undefined;
4914
+ website?: string | undefined;
4894
4915
  isTester?: boolean | undefined;
4895
4916
  canTestAdmin?: boolean | undefined;
4896
- publicProfile?: Record<string, import("./types").FirestoreValue> | undefined;
4917
+ instagram?: string | undefined;
4918
+ facebook?: string | undefined;
4919
+ twitter?: string | undefined;
4920
+ linkedin?: string | undefined;
4897
4921
  }>;
4898
4922
  readonly employeeInvite: import("zod").ZodObject<{
4899
4923
  email: import("zod").ZodString;