@mohasinac/appkit 4.41.7 β†’ 4.41.9

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.
@@ -77,7 +77,21 @@ export async function handleOrderCreate(input, ctx) {
77
77
  const firstItem = items[0];
78
78
  const firstItemName = firstItem?.productTitle ?? "an item";
79
79
  const additionalItemCount = Math.max(0, items.length - 1);
80
- const buyerName = order.userName ?? "A customer";
80
+ /*
81
+ * πŸ›‘ DECRYPT. This handler receives the RAW Firestore snapshot β€” the type
82
+ * above says so β€” so it never passes through the repository's `mapDoc`, and
83
+ * `userName` is PII, stored as `enc:v1:…`.
84
+ *
85
+ * Undecrypted, the ciphertext was interpolated straight into the announcement
86
+ * body and PERSISTED there, so every admin alert read
87
+ * "πŸ›οΈ New order! enc:v1:67Mn0rITjTWGsZZE:… purchased …". Measured on
88
+ * production: 383 such rows. The message is baked at write time, so the
89
+ * damage is permanent per row β€” decrypting on read would not repair them.
90
+ *
91
+ * `decryptPii` was already imported in this file and used for the store
92
+ * owner's phone eleven lines up; the buyer's name was simply missed.
93
+ */
94
+ const buyerName = decryptPii(order.userName) ?? order.userName ?? "A customer";
81
95
  const message = buildPurchaseAnnouncementMessage({
82
96
  buyerName,
83
97
  firstItemName,
@@ -54,14 +54,24 @@ function AdminCardItem({ row, view, selected, onToggleSelect, onRowClick, render
54
54
  return (_jsxs(Row, { gap: "sm", className: [
55
55
  "px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] cursor-pointer transition-colors hover:bg-surface-hover",
56
56
  selected ? "bg-primary-50/40 dark:bg-primary-900/10" : "",
57
- ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
57
+ ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => {
58
+ e.stopPropagation();
59
+ if (e.target === e.currentTarget)
60
+ onToggleSelect(row.id);
61
+ }, children: _jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }) })), _jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon }), _jsxs(Stack, { gap: "none", className: "flex-1 min-w-0", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId }))] }), flags.length > 0 && (_jsx(Row, { gap: "xs", className: "hidden sm:flex shrink-0", children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) })), _jsx(StatusBadge, { status: row.status }), _jsx(Span, { size: "xs", color: "muted", className: "hidden sm:block shrink-0 w-24", align: "end", children: row.updatedAt }), renderRowActions && (_jsx(Div, { "data-no-row-click": true, className: "shrink-0", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] }));
58
62
  }
59
63
  return (_jsxs(Div, { rounded: "xl", className: [
60
64
  "border overflow-hidden cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5",
61
65
  selected
62
66
  ? "border-primary ring-1 ring-primary/20 bg-primary-50/30 dark:bg-primary-900/10"
63
67
  : " border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]",
64
- ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm", onClick: (e) => { e.stopPropagation(); onToggleSelect(row.id); }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
68
+ ].filter(Boolean).join(" "), onClick: handleClick, role: onRowClick ? "button" : undefined, children: [onToggleSelect && (_jsxs(Row, { gap: "xs", "data-no-row-click": true, paddingY: "t-sm", padding: "x-sm",
69
+ // Same double-toggle guard as the compact row above β€” see that comment.
70
+ onClick: (e) => {
71
+ e.stopPropagation();
72
+ if (e.target === e.currentTarget)
73
+ onToggleSelect(row.id);
74
+ }, children: [_jsx(Checkbox, { bare: true, checked: selected, onChange: () => onToggleSelect(row.id), className: "h-4 w-4 rounded border-zinc-300 text-primary accent-primary cursor-pointer", "aria-label": `Select ${row.primary}` }), flags.length > 0 && (_jsx(Row, { gap: "xs", wrap: true, children: flags.map(({ key, label, tone }) => (_jsx(Span, { padding: "pill-2xs", weight: "medium", className: `inline-flex text-[10px] ${STATUS_TONE_CLASSES[tone]}`, rounded: "full", children: label }, key))) }))] })), _jsxs(Stack, { gap: "xs", className: `${__P.p3}.5`, children: [_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(RowAvatar, { image: row.image, alt: row.primary, icon: resourceIcon, size: "12" }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "semibold", className: "line-clamp-2 leading-snug", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", className: "truncate", children: row.secondary })] })] }), row.barcodeId && (_jsx(Text, { size: "xs", color: "faint", className: "truncate font-mono", children: row.barcodeId })), _jsxs(Row, { justify: "between", gap: "xs", children: [_jsx(StatusBadge, { status: row.status }), _jsx(Span, { color: "muted", className: "text-[11px] shrink-0", children: row.updatedAt })] }), renderRowActions && (_jsx(Row, { "data-no-row-click": true, justify: "end", gap: "xs", className: "border-t border-[var(--appkit-color-border)] -mx-[var(--appkit-space-3)] -mb-[var(--appkit-space-3)] mt-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)]", onClick: (e) => e.stopPropagation(), children: renderRowActions(row) }))] })] }));
65
75
  }
66
76
  export function AdminViewCards({ rows, view, isLoading, emptyLabel = "No items found", onRowClick, selectedIdSet, onToggleSelect, renderRowActions, resourceIcon, }) {
67
77
  if (isLoading) {
@@ -15,7 +15,6 @@ import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/of
15
15
  import { PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "../../../_internal/shared/media/limits";
16
16
  import { isFinalSale } from "../../products/constants/final-sale";
17
17
  import { normalizeError } from "../../../errors/normalize";
18
- import { toUserMessage } from "../../../errors/error-display-map";
19
18
  /**
20
19
  * "bundle" is a categoryType, not a ListingType, so it can't live in
21
20
  * LISTING_TYPE_REGISTRY β€” this is the single, explicit escape hatch that
@@ -351,6 +350,29 @@ const EDIT_SECTIONS = [
351
350
  { id: "publish", label: "Publish" },
352
351
  ];
353
352
  // ── Main SellerProductShell ───────────────────────────────────────────────
353
+ /**
354
+ * The message for a failed save.
355
+ *
356
+ * πŸ›‘ "Fix the highlighted errors" may ONLY be said when something is actually
357
+ * highlighted. `toUserMessage` short-circuits to its `fallback` whenever no
358
+ * translator is passed (`if (!t) return generic`), so passing that sentence as
359
+ * the fallback rendered it for EVERY failure code β€” an authorization or
360
+ * not-found refusal was reported to the seller as a validation problem, with
361
+ * nothing marked and nothing to act on.
362
+ *
363
+ * With no issues we say so plainly and quote the CODE. A code is a stable
364
+ * identifier, not server text, so this does not reintroduce the raw-message
365
+ * leak Rule #9 Β§6 forbids β€” it is the same reasoning that puts the error digest
366
+ * in front of users (Root Cause #78): an opaque handle they can quote beats an
367
+ * explanation that is wrong.
368
+ */
369
+ function saveFailureMessage(code, issueCount) {
370
+ if (issueCount > 0)
371
+ return "Fix the highlighted errors and try again.";
372
+ return code
373
+ ? `Could not save β€” the server refused this change (${code}). Nothing was changed.`
374
+ : "Could not save β€” the server refused this change. Nothing was changed.";
375
+ }
354
376
  export function SellerProductShell({ mode, listingType = "standard", initialValues,
355
377
  /*
356
378
  * `productId` was declared on the props interface and NEVER DESTRUCTURED β€”
@@ -426,9 +448,7 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
426
448
  }
427
449
  applyZodIssues(result.issues ?? [], setFieldError);
428
450
  if (!silent) {
429
- showToast(toUserMessage(result.code, undefined, {
430
- fallback: "Fix the highlighted errors and try again.",
431
- }), "error");
451
+ showToast(saveFailureMessage(result.code, (result.issues ?? []).length), "error");
432
452
  }
433
453
  }
434
454
  catch (err) {
@@ -469,9 +489,7 @@ productId, onSave, onPublish, onDiscard, isLoading = false, storeSlug, renderCat
469
489
  return;
470
490
  }
471
491
  applyZodIssues(result.issues ?? [], setFieldError);
472
- showToast(toUserMessage(result.code, undefined, {
473
- fallback: "Fix the highlighted errors and try again.",
474
- }), "error");
492
+ showToast(saveFailureMessage(result.code, (result.issues ?? []).length), "error");
475
493
  }
476
494
  catch (err) {
477
495
  void normalizeError(err);
package/dist/index.d.ts CHANGED
@@ -3040,7 +3040,7 @@ export { formatMonthYear } from "./utils/date.formatter";
3040
3040
  export { generateMediaFilename } from "./utils/id-generators";
3041
3041
  export { deriveContextTypeFromFilename } from "./utils/id-generators";
3042
3042
  export type { MediaFilenameContext } from "./utils/id-generators";
3043
- export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
3043
+ export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, getYouTubeVideoId, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
3044
3044
  export { generateFAQId } from "./utils/id-generators";
3045
3045
  export type { GenerateFAQIdInput } from "./utils/id-generators";
3046
3046
  export { generateCroppedImageFilename } from "./utils/id-generators";
package/dist/index.js CHANGED
@@ -5401,7 +5401,7 @@ export { formatCurrency, formatFileSize } from "./utils/number.formatter";
5401
5401
  export { formatMonthYear } from "./utils/date.formatter";
5402
5402
  export { generateMediaFilename } from "./utils/id-generators"; // generateProductImageFilename already exported from "./utils/id-generators";
5403
5403
  export { deriveContextTypeFromFilename } from "./utils/id-generators";
5404
- export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
5404
+ export { resolveMediaUrl, resolveVideoUrl, FIREBASE_STORAGE_HOST, GCS_HOST, isStoredMediaRef, getYouTubeVideoId, MEDIA_PROXY_PREFIX, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, APPROVED_MEDIA_DOMAINS, } from "./utils/media-url";
5405
5405
  export { generateFAQId } from "./utils/id-generators";
5406
5406
  export { generateCroppedImageFilename } from "./utils/id-generators";
5407
5407
  // D5 + VC7 buyer↔seller conversations were deleted 2026-08-31 (W11/D16). The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.7",
3
+ "version": "4.41.9",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"