@mohasinac/appkit 4.31.1 → 4.32.1

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 (71) 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/account/components/UserBidsView.js +6 -1
  6. package/dist/features/account/components/UserOrdersView.js +8 -1
  7. package/dist/features/account/components/UserReturnsView.js +6 -1
  8. package/dist/features/admin/components/AdminAdsView.js +36 -34
  9. package/dist/features/admin/components/AdminAuditLogView.js +12 -1
  10. package/dist/features/admin/components/AdminBundlesView.js +5 -1
  11. package/dist/features/admin/components/AdminCarouselEditorView.js +78 -10
  12. package/dist/features/admin/components/AdminFaqsView.js +10 -1
  13. package/dist/features/admin/components/AdminNotificationsView.js +7 -1
  14. package/dist/features/admin/components/AdminOrderEditorView.js +55 -24
  15. package/dist/features/admin/components/AdminPrizeDrawsView.js +11 -1
  16. package/dist/features/admin/components/AdminProductEditorView.js +78 -18
  17. package/dist/features/admin/components/AdminSectionsView.js +89 -32
  18. package/dist/features/admin/components/AdminSiteSettingsView.js +96 -44
  19. package/dist/features/admin/components/AdminStoresView.js +13 -1
  20. package/dist/features/admin/components/AdminSupportTicketDetailView.js +58 -43
  21. package/dist/features/admin/components/AdminSupportTicketsView.js +11 -1
  22. package/dist/features/admin/components/AdminTeamView.js +11 -1
  23. package/dist/features/admin/components/AdminTesterChecklistView.js +6 -1
  24. package/dist/features/admin/components/AdminUserEditorView.js +65 -82
  25. package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +6 -1
  26. package/dist/features/admin/constants/admin-page-tabs.d.ts +36 -0
  27. package/dist/features/admin/constants/admin-page-tabs.js +45 -0
  28. package/dist/features/admin/schemas/admin-ops-forms.d.ts +81 -0
  29. package/dist/features/admin/schemas/admin-ops-forms.js +123 -1
  30. package/dist/features/admin/schemas/admin-user-form.d.ts +78 -3
  31. package/dist/features/admin/schemas/admin-user-form.js +96 -7
  32. package/dist/features/admin/schemas/homepage-section-form.d.ts +9 -0
  33. package/dist/features/layout/PageTabs.d.ts +17 -0
  34. package/dist/features/layout/PageTabs.js +45 -0
  35. package/dist/features/products/components/GroupSettingsPanel.js +62 -16
  36. package/dist/features/search/action-index/tab-entries.d.ts +18 -0
  37. package/dist/features/search/action-index/tab-entries.js +128 -0
  38. package/dist/features/seller/components/SellerBundlesView.js +5 -1
  39. package/dist/features/seller/components/SellerGoogleReviewsView.js +31 -19
  40. package/dist/features/seller/components/SellerOrdersView.js +57 -19
  41. package/dist/features/seller/components/SellerProductsView.js +10 -1
  42. package/dist/features/seller/components/SellerReviewsView.d.ts +2 -1
  43. package/dist/features/seller/components/SellerReviewsView.js +54 -19
  44. package/dist/features/seller/constants/store-tabs.d.ts +64 -0
  45. package/dist/features/seller/constants/store-tabs.js +60 -0
  46. package/dist/features/seller/schemas/order-forms.d.ts +33 -0
  47. package/dist/features/seller/schemas/order-forms.js +75 -0
  48. package/dist/features/seller/schemas/review-forms.d.ts +37 -0
  49. package/dist/features/seller/schemas/review-forms.js +82 -0
  50. package/dist/features/shell/index.d.ts +1 -1
  51. package/dist/features/shell/index.js +5 -1
  52. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +44 -15
  53. package/dist/features/shipments/schemas/validation.d.ts +41 -0
  54. package/dist/features/shipments/schemas/validation.js +49 -0
  55. package/dist/features/store-extensions/schemas/google-config-form.d.ts +36 -0
  56. package/dist/features/store-extensions/schemas/google-config-form.js +24 -0
  57. package/dist/features/stores/actions/store-query-actions.js +5 -3
  58. package/dist/features/stores/repository/store.repository.d.ts +3 -1
  59. package/dist/features/stores/repository/store.repository.js +6 -2
  60. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +79 -0
  61. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +53 -22
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +2 -0
  64. package/dist/next/index.d.ts +2 -0
  65. package/dist/next/index.js +1 -0
  66. package/dist/react/hooks/useTabParam.d.ts +19 -0
  67. package/dist/react/hooks/useTabParam.js +65 -0
  68. package/dist/schemas/registry.d.ts +27 -3
  69. package/dist/styles.css +1 -1
  70. package/dist/tailwind-utilities.css +1 -1
  71. package/package.json +1 -1
@@ -173,3 +173,84 @@ export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
173
173
  thirdPartyUrl?: string | undefined;
174
174
  }>;
175
175
  export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
176
+ /**
177
+ * Ad SETTINGS — the provider credentials and the global consent switch.
178
+ *
179
+ * Distinct from `adminAdFormSchema`, which is one ad record. This is the
180
+ * platform-level configuration every ad renders under, and it was validated by
181
+ * a hand-written `localCredentialIssues` array that computed the same two rules
182
+ * on every keystroke and rendered them as a banner headed "Fix settings before
183
+ * saving" — never on the field that was wrong.
184
+ *
185
+ * Both credentials are optional and BLANK MEANS UNCHANGED: the route stores
186
+ * them encrypted and returns only a masked form, so the input starts empty on
187
+ * every load and an empty submit must leave the stored value alone. That is why
188
+ * neither field has a `.min()` — an empty string here is not an omission.
189
+ */
190
+ export declare const adsConfigSchema: z.ZodEffects<z.ZodObject<{
191
+ consentRequired: z.ZodBoolean;
192
+ adsenseClientId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
193
+ thirdPartyScriptUrl: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
194
+ }, "strip", z.ZodTypeAny, {
195
+ consentRequired: boolean;
196
+ adsenseClientId?: string | undefined;
197
+ thirdPartyScriptUrl?: string | undefined;
198
+ }, {
199
+ consentRequired: boolean;
200
+ adsenseClientId?: string | undefined;
201
+ thirdPartyScriptUrl?: string | undefined;
202
+ }>, {
203
+ consentRequired: boolean;
204
+ adsenseClientId?: string | undefined;
205
+ thirdPartyScriptUrl?: string | undefined;
206
+ }, {
207
+ consentRequired: boolean;
208
+ adsenseClientId?: string | undefined;
209
+ thirdPartyScriptUrl?: string | undefined;
210
+ }>;
211
+ export type AdsConfigValues = z.infer<typeof adsConfigSchema>;
212
+ /**
213
+ * The support-ticket triage form — what an admin CHANGES about a ticket, as
214
+ * distinct from what they SAY on it (the reply box, which is its own write to
215
+ * a different endpoint and carries the ticket's new status with it).
216
+ *
217
+ * The five linked-party ids are FLAT here and reassembled into
218
+ * `relatedParties` at the payload boundary, the same call the ad creative and
219
+ * the user's public profile make: the stored shape is one nested object, and
220
+ * the form has always offered five discrete inputs.
221
+ */
222
+ export declare const supportTicketUpdateSchema: z.ZodObject<{
223
+ status: z.ZodEnum<["open", "in_progress", "waiting_on_user", "resolved", "closed"]>;
224
+ priority: z.ZodEnum<["low", "normal", "high", "urgent"]>;
225
+ internalNotes: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
226
+ userId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
227
+ storeId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
228
+ orderId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
229
+ productId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
230
+ bidId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
231
+ }, "strip", z.ZodTypeAny, {
232
+ status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
233
+ priority: "normal" | "high" | "low" | "urgent";
234
+ storeId?: string | undefined;
235
+ userId?: string | undefined;
236
+ orderId?: string | undefined;
237
+ productId?: string | undefined;
238
+ bidId?: string | undefined;
239
+ internalNotes?: string | undefined;
240
+ }, {
241
+ status: "closed" | "open" | "resolved" | "in_progress" | "waiting_on_user";
242
+ priority: "normal" | "high" | "low" | "urgent";
243
+ storeId?: string | undefined;
244
+ userId?: string | undefined;
245
+ orderId?: string | undefined;
246
+ productId?: string | undefined;
247
+ bidId?: string | undefined;
248
+ internalNotes?: string | undefined;
249
+ }>;
250
+ export type SupportTicketUpdateValues = z.infer<typeof supportTicketUpdateSchema>;
251
+ /**
252
+ * The five flat id fields, back into the nested `relatedParties` the ticket
253
+ * stores. Blank fields are dropped, and an entirely empty set returns
254
+ * `undefined` so the key is omitted rather than written as `{}`.
255
+ */
256
+ export declare function toRelatedPartiesPayload(v: SupportTicketUpdateValues): Record<string, string> | undefined;
@@ -40,7 +40,11 @@ export const adminOrderUpdateSchema = z.object({
40
40
  section: "shipping", sectionLabel: "Shipping", order: 1, row: "pair",
41
41
  }),
42
42
  carrier: annotate(z.string().trim().max(80).optional(), {
43
- section: "shipping", order: 2, row: "pair",
43
+ // `kind: "select"` is explicit because the field is a free string in the
44
+ // schema — the route accepts any carrier name — while the ADMIN form offers
45
+ // a fixed list. Without it the inferred control is a text input and the
46
+ // `options` passed by the view would be ignored.
47
+ section: "shipping", order: 2, row: "pair", kind: "select",
44
48
  }),
45
49
  /*
46
50
  * 🛑 Bounded. This was `parseFloat` + `> 0` with no ceiling, so a slipped
@@ -229,3 +233,121 @@ export const adminAdFormSchema = z
229
233
  }
230
234
  }
231
235
  });
236
+ /**
237
+ * Ad SETTINGS — the provider credentials and the global consent switch.
238
+ *
239
+ * Distinct from `adminAdFormSchema`, which is one ad record. This is the
240
+ * platform-level configuration every ad renders under, and it was validated by
241
+ * a hand-written `localCredentialIssues` array that computed the same two rules
242
+ * on every keystroke and rendered them as a banner headed "Fix settings before
243
+ * saving" — never on the field that was wrong.
244
+ *
245
+ * Both credentials are optional and BLANK MEANS UNCHANGED: the route stores
246
+ * them encrypted and returns only a masked form, so the input starts empty on
247
+ * every load and an empty submit must leave the stored value alone. That is why
248
+ * neither field has a `.min()` — an empty string here is not an omission.
249
+ */
250
+ export const adsConfigSchema = z.object({
251
+ consentRequired: annotate(z.boolean(), {
252
+ section: "consent", sectionLabel: "Consent", sectionRequired: true,
253
+ quick: true, order: 1, row: "full",
254
+ label: "Require consent globally for ad rendering",
255
+ }),
256
+ adsenseClientId: annotate(z
257
+ .string()
258
+ .trim()
259
+ .regex(/^ca-pub-[0-9]{10,20}$/, "An AdSense client id looks like ca-pub-XXXXXXXXXX.")
260
+ .optional()
261
+ .or(z.literal("")), {
262
+ section: "credentials", sectionLabel: "Provider credentials",
263
+ order: 1, row: "pair", label: "AdSense client id",
264
+ help: "Leave blank to keep the stored value.",
265
+ }),
266
+ thirdPartyScriptUrl: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
267
+ section: "credentials", order: 2, row: "pair",
268
+ label: "Third-party script URL", inputType: "url",
269
+ help: "Must be https. Leave blank to keep the stored value.",
270
+ }),
271
+ })
272
+ .superRefine((v, ctx) => {
273
+ const url = v.thirdPartyScriptUrl?.trim();
274
+ if (!url)
275
+ return;
276
+ let parsed = null;
277
+ try {
278
+ parsed = new URL(url);
279
+ }
280
+ catch (err) {
281
+ void normalizeError(err);
282
+ parsed = null;
283
+ }
284
+ if (!parsed) {
285
+ ctx.addIssue({
286
+ code: z.ZodIssueCode.custom, path: ["thirdPartyScriptUrl"],
287
+ message: "That is not a valid URL.",
288
+ });
289
+ }
290
+ else if (parsed.protocol !== "https:") {
291
+ // A third-party ad script runs in the page's own origin; http would let
292
+ // any network hop rewrite it.
293
+ ctx.addIssue({
294
+ code: z.ZodIssueCode.custom, path: ["thirdPartyScriptUrl"],
295
+ message: "A third-party script URL must use https.",
296
+ });
297
+ }
298
+ });
299
+ /**
300
+ * The support-ticket triage form — what an admin CHANGES about a ticket, as
301
+ * distinct from what they SAY on it (the reply box, which is its own write to
302
+ * a different endpoint and carries the ticket's new status with it).
303
+ *
304
+ * The five linked-party ids are FLAT here and reassembled into
305
+ * `relatedParties` at the payload boundary, the same call the ad creative and
306
+ * the user's public profile make: the stored shape is one nested object, and
307
+ * the form has always offered five discrete inputs.
308
+ */
309
+ export const supportTicketUpdateSchema = z.object({
310
+ status: annotate(z.enum(["open", "in_progress", "waiting_on_user", "resolved", "closed"]), {
311
+ section: "triage", sectionLabel: "Triage", sectionRequired: true,
312
+ quick: true, order: 1, row: "pair", kind: "select",
313
+ }),
314
+ priority: annotate(z.enum(["low", "normal", "high", "urgent"]), {
315
+ section: "triage", quick: true, order: 2, row: "pair", kind: "select",
316
+ }),
317
+ internalNotes: annotate(z.string().trim().max(4000).optional().or(z.literal("")), {
318
+ section: "triage", order: 3, row: "full", kind: "textarea",
319
+ label: "Internal notes (staff only)",
320
+ help: "Visible only to admins and employees.",
321
+ }),
322
+ userId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
323
+ section: "parties", sectionLabel: "Linked parties", order: 1, row: "pair",
324
+ label: "User slug",
325
+ help: "Tag the buyer, store, order, product or bid this ticket concerns.",
326
+ }),
327
+ storeId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
328
+ section: "parties", order: 2, row: "pair", label: "Store slug",
329
+ }),
330
+ orderId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
331
+ section: "parties", order: 3, row: "pair", label: "Order ID",
332
+ }),
333
+ productId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
334
+ section: "parties", order: 4, row: "pair", label: "Product slug",
335
+ }),
336
+ bidId: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
337
+ section: "parties", order: 5, row: "pair", label: "Bid ID",
338
+ }),
339
+ });
340
+ /**
341
+ * The five flat id fields, back into the nested `relatedParties` the ticket
342
+ * stores. Blank fields are dropped, and an entirely empty set returns
343
+ * `undefined` so the key is omitted rather than written as `{}`.
344
+ */
345
+ export function toRelatedPartiesPayload(v) {
346
+ const out = {};
347
+ for (const key of ["userId", "storeId", "orderId", "productId", "bidId"]) {
348
+ const value = v[key]?.trim();
349
+ if (value)
350
+ out[key] = value;
351
+ }
352
+ return Object.keys(out).length > 0 ? out : undefined;
353
+ }
@@ -1,26 +1,69 @@
1
1
  import { z } from "zod";
2
+ import type { JsonValue } from "../../../schemas/types";
2
3
  export declare const adminUserUpdateSchema: z.ZodObject<{
4
+ role: z.ZodOptional<z.ZodEnum<["user", "seller", "admin", "moderator"]>>;
5
+ emailVerified: z.ZodOptional<z.ZodBoolean>;
3
6
  displayName: z.ZodOptional<z.ZodString>;
4
7
  phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
5
8
  isTester: z.ZodOptional<z.ZodBoolean>;
6
9
  canTestAdmin: z.ZodOptional<z.ZodBoolean>;
7
10
  adminNotes: z.ZodOptional<z.ZodString>;
8
- publicProfile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
11
+ bio: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
12
+ location: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
13
+ website: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
14
+ twitter: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
15
+ instagram: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
16
+ facebook: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
17
+ linkedin: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
9
18
  }, "strip", z.ZodTypeAny, {
19
+ role?: "user" | "seller" | "admin" | "moderator" | undefined;
10
20
  displayName?: string | undefined;
21
+ location?: string | undefined;
11
22
  adminNotes?: string | undefined;
23
+ bio?: string | undefined;
12
24
  phoneNumber?: string | undefined;
25
+ emailVerified?: boolean | undefined;
26
+ website?: string | undefined;
13
27
  isTester?: boolean | undefined;
14
28
  canTestAdmin?: boolean | undefined;
15
- publicProfile?: Record<string, import("../../..").FirestoreValue> | undefined;
29
+ instagram?: string | undefined;
30
+ facebook?: string | undefined;
31
+ twitter?: string | undefined;
32
+ linkedin?: string | undefined;
16
33
  }, {
34
+ role?: "user" | "seller" | "admin" | "moderator" | undefined;
17
35
  displayName?: string | undefined;
36
+ location?: string | undefined;
18
37
  adminNotes?: string | undefined;
38
+ bio?: string | undefined;
19
39
  phoneNumber?: string | undefined;
40
+ emailVerified?: boolean | undefined;
41
+ website?: string | undefined;
20
42
  isTester?: boolean | undefined;
21
43
  canTestAdmin?: boolean | undefined;
22
- publicProfile?: Record<string, import("../../..").FirestoreValue> | undefined;
44
+ instagram?: string | undefined;
45
+ facebook?: string | undefined;
46
+ twitter?: string | undefined;
47
+ linkedin?: string | undefined;
23
48
  }>;
49
+ /**
50
+ * The flat social/profile fields, reassembled into the nested shape the PATCH
51
+ * route stores.
52
+ *
53
+ * Empty strings are dropped rather than written: the drawer sends a partial
54
+ * PATCH so an untouched field must leave the stored subkey alone, and writing
55
+ * `""` would erase it. Returns `undefined` when nothing was filled in, so the
56
+ * key is omitted from the payload entirely.
57
+ */
58
+ export declare function toPublicProfilePayload(v: {
59
+ bio?: string;
60
+ location?: string;
61
+ website?: string;
62
+ twitter?: string;
63
+ instagram?: string;
64
+ facebook?: string;
65
+ linkedin?: string;
66
+ }): Record<string, JsonValue> | undefined;
24
67
  export type AdminUserUpdateValues = z.infer<typeof adminUserUpdateSchema>;
25
68
  /**
26
69
  * A store's WhatsApp Business connection.
@@ -51,6 +94,38 @@ export declare const whatsappSettingsSchema: z.ZodObject<{
51
94
  catalogId?: string | undefined;
52
95
  }>;
53
96
  export type WhatsappSettingsValues = z.infer<typeof whatsappSettingsSchema>;
97
+ /**
98
+ * The CONNECTION form's fields — credentials only.
99
+ *
100
+ * `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
101
+ * card and owns its own write (`syncToggleMutation`), because the credentials
102
+ * form used to send it unconditionally from two cards away: a seller flipped
103
+ * the switch, saw it move, and nothing persisted it. A field in this schema is
104
+ * a field this form's Save button owns.
105
+ *
106
+ * 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
107
+ * carries the SAME field instances into the new shape, which is what keeps the
108
+ * `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
109
+ * transparent for the same reason; a field-level re-wrap is not.)
110
+ */
111
+ export declare const whatsappConnectionSchema: z.ZodObject<Omit<{
112
+ catalogSyncEnabled: z.ZodBoolean;
113
+ phoneNumber: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
114
+ wabaId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
115
+ catalogId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
116
+ accessToken: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
117
+ }, "catalogSyncEnabled">, "strip", z.ZodTypeAny, {
118
+ phoneNumber?: string | undefined;
119
+ accessToken?: string | undefined;
120
+ wabaId?: string | undefined;
121
+ catalogId?: string | undefined;
122
+ }, {
123
+ phoneNumber?: string | undefined;
124
+ accessToken?: string | undefined;
125
+ wabaId?: string | undefined;
126
+ catalogId?: string | undefined;
127
+ }>;
128
+ export type WhatsappConnectionValues = z.infer<typeof whatsappConnectionSchema>;
54
129
  /**
55
130
  * A buyer's lottery entry.
56
131
  *
@@ -30,13 +30,26 @@
30
30
  */
31
31
  import { z } from "zod";
32
32
  import { annotate } from "../../shell/field-ui-meta";
33
- import { firestoreValueSchema } from "../../../schemas/firestore-value";
34
33
  const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
35
34
  export const adminUserUpdateSchema = z.object({
35
+ /*
36
+ * Role and email-verification lead, because they are what an admin opens this
37
+ * drawer for. They were controls with no schema entry at all — the form
38
+ * declared a schema covering six of its twelve fields, so `Role` and
39
+ * `Email verified` were validated by nothing.
40
+ */
41
+ role: annotate(z.enum(["user", "seller", "admin", "moderator"]).optional(), {
42
+ section: "profile", sectionLabel: "Profile", sectionRequired: true, quick: true,
43
+ order: 1, row: "pair", kind: "select",
44
+ }),
45
+ emailVerified: annotate(z.boolean().optional(), {
46
+ section: "profile", quick: true, order: 2, row: "quarter",
47
+ label: "Email verified",
48
+ }),
36
49
  displayName: annotate(z.string().trim().min(1).max(120).optional(), {
37
- section: "profile", sectionLabel: "Profile", sectionRequired: true, quick: true, order: 1, row: "pair",
50
+ section: "profile", quick: true, order: 3, row: "pair",
38
51
  }),
39
- phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "profile", order: 2, row: "pair" }),
52
+ phoneNumber: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.").optional().or(z.literal("")), { section: "profile", order: 4, row: "pair" }),
40
53
  /*
41
54
  * The tester flags. Kept OPTIONAL rather than defaulted, deliberately: a
42
55
  * PATCH that omits them must leave them alone. Defaulting either to `false`
@@ -48,17 +61,76 @@ export const adminUserUpdateSchema = z.object({
48
61
  }),
49
62
  canTestAdmin: annotate(z.boolean().optional(), {
50
63
  section: "access", order: 2, row: "quarter",
64
+ // The drawer already hid this behind `isTester`; stating it here is what
65
+ // makes `visibleValues()` drop a stale `true` when the flag is switched off.
66
+ when: (v) => Boolean(v.isTester),
51
67
  }),
52
68
  adminNotes: annotate(z.string().trim().max(2000).optional(), {
53
69
  section: "access", order: 3, row: "full", kind: "textarea",
54
70
  help: "Internal only — never shown to the user.",
55
71
  }),
56
- // Open: the public profile carries per-user social links and bio fields that
57
- // are added over time, and a closed shape would strip a newly-added one.
58
- publicProfile: annotate(z.record(firestoreValueSchema).optional(), {
59
- section: "access", order: 4, row: "full", kind: "list",
72
+ /*
73
+ * The public profile, FLAT — the same call the ad form makes.
74
+ *
75
+ * The STORED shape is an open record (`publicProfileSchema` on the route),
76
+ * because per-user profile keys are added over time and a closed shape would
77
+ * strip a newly-added one. That was the right conclusion for the document and
78
+ * the wrong one for the FORM: as one `kind: "list"` field it renders as a
79
+ * disabled placeholder, while the drawer has always offered seven discrete
80
+ * inputs — bio, location, website and four social handles — validated by
81
+ * nothing. The nested shape is reassembled at the payload boundary, where the
82
+ * API wants it, by `toPublicProfilePayload()` below.
83
+ */
84
+ bio: annotate(z.string().trim().max(1000).optional().or(z.literal("")), {
85
+ section: "public", sectionLabel: "Public profile", order: 1, row: "full",
86
+ kind: "textarea", help: "Shown on the user's public profile page.",
87
+ }),
88
+ location: annotate(z.string().trim().max(120).optional().or(z.literal("")), {
89
+ section: "public", order: 2, row: "pair",
90
+ }),
91
+ website: annotate(z.string().trim().max(2000).optional().or(z.literal("")), {
92
+ section: "public", order: 3, row: "pair", inputType: "url",
93
+ }),
94
+ twitter: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
95
+ section: "social", sectionLabel: "Social links", order: 1, row: "pair",
96
+ label: "Twitter / X",
97
+ }),
98
+ instagram: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
99
+ section: "social", order: 2, row: "pair",
100
+ }),
101
+ facebook: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
102
+ section: "social", order: 3, row: "pair",
103
+ }),
104
+ linkedin: annotate(z.string().trim().max(200).optional().or(z.literal("")), {
105
+ section: "social", order: 4, row: "pair", label: "LinkedIn",
60
106
  }),
61
107
  });
108
+ /**
109
+ * The flat social/profile fields, reassembled into the nested shape the PATCH
110
+ * route stores.
111
+ *
112
+ * Empty strings are dropped rather than written: the drawer sends a partial
113
+ * PATCH so an untouched field must leave the stored subkey alone, and writing
114
+ * `""` would erase it. Returns `undefined` when nothing was filled in, so the
115
+ * key is omitted from the payload entirely.
116
+ */
117
+ export function toPublicProfilePayload(v) {
118
+ const socialLinks = {};
119
+ for (const key of ["twitter", "instagram", "facebook", "linkedin"]) {
120
+ const value = v[key]?.trim();
121
+ if (value)
122
+ socialLinks[key] = value;
123
+ }
124
+ const publicProfile = {};
125
+ for (const key of ["bio", "location", "website"]) {
126
+ const value = v[key]?.trim();
127
+ if (value)
128
+ publicProfile[key] = value;
129
+ }
130
+ if (Object.keys(socialLinks).length > 0)
131
+ publicProfile.socialLinks = socialLinks;
132
+ return Object.keys(publicProfile).length > 0 ? publicProfile : undefined;
133
+ }
62
134
  /**
63
135
  * A store's WhatsApp Business connection.
64
136
  *
@@ -79,6 +151,23 @@ export const whatsappSettingsSchema = z.object({
79
151
  catalogId: annotate(z.string().trim().regex(/^\d{5,32}$/, "A catalog ID is a numeric id, not a URL.").optional().or(z.literal("")), { section: "connection", order: 3, row: "pair" }),
80
152
  accessToken: annotate(z.string().trim().min(20, "That access token looks truncated.").optional().or(z.literal("")), { section: "connection", order: 4, row: "full", help: "Leave blank to keep the current token." }),
81
153
  });
154
+ /**
155
+ * The CONNECTION form's fields — credentials only.
156
+ *
157
+ * `catalogSyncEnabled` is deliberately absent. Its toggle lives in a different
158
+ * card and owns its own write (`syncToggleMutation`), because the credentials
159
+ * form used to send it unconditionally from two cards away: a seller flipped
160
+ * the switch, saw it move, and nothing persisted it. A field in this schema is
161
+ * a field this form's Save button owns.
162
+ *
163
+ * 🛑 `.omit()` rather than a second literal, so the two cannot drift — it
164
+ * carries the SAME field instances into the new shape, which is what keeps the
165
+ * `annotate()` WeakMap entries alive. (A wrapper such as `.superRefine()` is
166
+ * transparent for the same reason; a field-level re-wrap is not.)
167
+ */
168
+ export const whatsappConnectionSchema = whatsappSettingsSchema.omit({
169
+ catalogSyncEnabled: true,
170
+ });
82
171
  /**
83
172
  * A buyer's lottery entry.
84
173
  *
@@ -20,3 +20,12 @@ export declare const homepageSectionFormSchema: z.ZodObject<{
20
20
  order?: number | undefined;
21
21
  }>;
22
22
  export type HomepageSectionFormValues = z.infer<typeof homepageSectionFormSchema>;
23
+ /**
24
+ * What the FORM holds, as distinct from what the schema produces.
25
+ *
26
+ * `order` is a `z.coerce.number()`, so the parsed value is a number while the
27
+ * control that feeds it is a text input holding a string — including the empty
28
+ * string, which means "auto-place". The draft is the INPUT side; only the save
29
+ * path sees the output.
30
+ */
31
+ export type HomepageSectionFormInput = z.input<typeof homepageSectionFormSchema>;
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ export interface PageTabDef {
3
+ id: string;
4
+ label: string;
5
+ }
6
+ export interface PageTabsProps {
7
+ /** The tabs, in order. The first is the default when `?tab=` is absent or unknown. */
8
+ tabs: readonly PageTabDef[];
9
+ /** One node per tab id. A missing entry renders an empty panel, not a crash. */
10
+ panels: Record<string, React.ReactNode>;
11
+ /**
12
+ * Override the default tab. Omit to use the first — which is right whenever
13
+ * the host page's own content is the first tab, as it is on all six.
14
+ */
15
+ defaultTabId?: string;
16
+ }
17
+ export declare function PageTabs({ tabs, panels, defaultTabId }: PageTabsProps): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /*
4
+ * WHY: W8's C2 folds sibling pages that are one task into one tabbed page, and
5
+ * four of the six hosts are SERVER components that fetch their panels'
6
+ * data. Tabs need client state, so something has to sit between.
7
+ *
8
+ * WHAT: The tab strip and the panel switch, URL-synced via `?tab=`.
9
+ *
10
+ * ## 🛑 Why the tab list is DATA and the guard is derived
11
+ *
12
+ * A Server Component cannot hand a function to a Client Component — it is not
13
+ * serialisable, and the failure is a build error at best and a React #441 at
14
+ * worst (Root Cause #76). So this takes the tab ARRAY and derives the
15
+ * "is this a real tab id" test from it, rather than taking an `isValid`
16
+ * predicate the way `useTabParam` does for callers that are already clients.
17
+ *
18
+ * Panels are passed as a record of nodes, which IS serialisable: a Server
19
+ * Component renders both panels and hands over the results.
20
+ *
21
+ * ## Every panel is rendered, always
22
+ *
23
+ * `TabsContent` mounts all of them and shows one. That is deliberate for these
24
+ * six pages: the absorbed panels are small, their data was already fetched
25
+ * server-side by the same request, and mounting on first view would make a tab
26
+ * click cost a spinner for content that is already in the payload.
27
+ *
28
+ * EXPORTS: PageTabs, type PageTabDef
29
+ *
30
+ * @tag domain:layout
31
+ * @tag layer:component
32
+ * @tag pattern:none
33
+ * @tag access:client
34
+ * @tag consumers:store payouts/shipping/fulfillment/storefront,admin roles/settings
35
+ * @tag sideEffects:router
36
+ */
37
+ import React from "react";
38
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../ui";
39
+ import { useTabParam } from "../../react/hooks/useTabParam";
40
+ export function PageTabs({ tabs, panels, defaultTabId }) {
41
+ const ids = React.useMemo(() => tabs.map((t) => t.id), [tabs]);
42
+ const isValid = React.useCallback((value) => !!value && ids.includes(value), [ids]);
43
+ const [activeTab, setActiveTab] = useTabParam(isValid, defaultTabId ?? tabs[0]?.id ?? "");
44
+ return (_jsxs(Tabs, { value: activeTab, onChange: setActiveTab, children: [_jsx(TabsList, { children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, children: tab.label }, tab.id))) }), tabs.map((tab) => (_jsx(TabsContent, { value: tab.id, children: panels[tab.id] ?? null }, tab.id)))] }));
45
+ }