@aglyn/plugins-commerce 1.0.0-beta.244 → 1.0.0-beta.247

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 (61) hide show
  1. package/README.md +4 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +10 -0
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/local-delivery-settings-card.component.js +10 -13
  6. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -1
  7. package/src/lib/components/console/locations-card.component.js +5 -9
  8. package/src/lib/components/console/locations-card.component.js.map +1 -1
  9. package/src/lib/components/console/product-zones.d.ts +7 -1
  10. package/src/lib/components/console/product-zones.js.map +1 -1
  11. package/src/lib/components/console/products-hub-zone.component.d.ts +3 -2
  12. package/src/lib/components/console/products-hub-zone.component.js +9 -1
  13. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  14. package/src/lib/components/console/review-settings-card.component.d.ts +14 -0
  15. package/src/lib/components/console/review-settings-card.component.js +168 -0
  16. package/src/lib/components/console/review-settings-card.component.js.map +1 -0
  17. package/src/lib/components/console/reviews-moderation-card.component.d.ts +14 -0
  18. package/src/lib/components/console/reviews-moderation-card.component.js +85 -11
  19. package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
  20. package/src/lib/components/product-reviews.d.ts +29 -1
  21. package/src/lib/components/product-reviews.js +97 -12
  22. package/src/lib/components/product-reviews.js.map +1 -1
  23. package/src/lib/model/commerce.d.ts +11 -2
  24. package/src/lib/model/commerce.js +14 -2
  25. package/src/lib/model/commerce.js.map +1 -1
  26. package/src/lib/model/index.d.ts +1 -0
  27. package/src/lib/model/index.js +1 -0
  28. package/src/lib/model/index.js.map +1 -1
  29. package/src/lib/model/product-reviews.d.ts +124 -0
  30. package/src/lib/model/product-reviews.js +133 -0
  31. package/src/lib/model/product-reviews.js.map +1 -0
  32. package/src/lib/model/store-setup-content.d.ts +145 -0
  33. package/src/lib/model/store-setup-content.js +813 -0
  34. package/src/lib/model/store-setup-content.js.map +1 -0
  35. package/src/lib/server/membership-reset.js +8 -2
  36. package/src/lib/server/membership-reset.js.map +1 -1
  37. package/src/lib/server/order-event-triggers.d.ts +2 -0
  38. package/src/lib/server/order-event-triggers.js +18 -1
  39. package/src/lib/server/order-event-triggers.js.map +1 -1
  40. package/src/lib/server/review-requests.d.ts +102 -0
  41. package/src/lib/server/review-requests.js +346 -0
  42. package/src/lib/server/review-requests.js.map +1 -0
  43. package/src/lib/server/review-tokens.d.ts +82 -0
  44. package/src/lib/server/review-tokens.js +119 -0
  45. package/src/lib/server/review-tokens.js.map +1 -0
  46. package/src/lib/server/reviews.d.ts +36 -3
  47. package/src/lib/server/reviews.js +310 -52
  48. package/src/lib/server/reviews.js.map +1 -1
  49. package/src/lib/server/store-setup-ai-capability.d.ts +61 -0
  50. package/src/lib/server/store-setup-ai-capability.js +343 -0
  51. package/src/lib/server/store-setup-ai-capability.js.map +1 -0
  52. package/src/lib/server/store-setup-ai-writer.console.d.ts +27 -0
  53. package/src/lib/server/store-setup-ai-writer.console.js +44 -0
  54. package/src/lib/server/store-setup-ai-writer.console.js.map +1 -0
  55. package/src/lib/server/store-setup-ai-writer.d.ts +37 -0
  56. package/src/lib/server/store-setup-ai-writer.js +174 -0
  57. package/src/lib/server/store-setup-ai-writer.js.map +1 -0
  58. package/src/lib/server.js +27 -0
  59. package/src/lib/server.js.map +1 -1
  60. package/src/lib/tenant-emails.js +58 -0
  61. package/src/lib/tenant-emails.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/reviews-moderation-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n MenuItem,\n Rating,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface ReviewsModerationCardProps {\n hostId: string\n}\n\n/**\n * Review moderation (AGL-324): pending queue with approve/reject and\n * inline seller replies; approved reviews render on the product-reviews\n * block with the aggregate in JSON-LD.\n *\n * Gated on `productReviews` since AGL-2080. `server/reviews.ts:108` enforces\n * it, and this card rendered identically on Free — an operator could work a\n * moderation queue whose approvals the renderer would never honour.\n */\n\nconst reviewsHelp = pluginDocsHelp('catalog', { anchor: '#product-reviews' })\n\nexport function ReviewsModerationCard(props: ReviewsModerationCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [filter, setFilter] = useState('pending')\n /*\n * The status filter is the QUERY, and the order is the server's.\n *\n * This read `limit(200)` with no `orderBy` and then filtered and sorted in\n * the browser. A limited Firestore query with no ordering returns\n * DOCUMENT-ID order, and reviews are written with `.add()`, so those two\n * hundred were a pseudo-random sample — \"Pending\" showed whichever pending\n * reviews happened to fall in it, which on a busy shop is not the queue and\n * cannot be worked through.\n *\n * Filtering here rather than in the browser is what lets the window shrink:\n * a page of 25 mixed reviews might hold three pending ones, and a card that\n * rendered those three would call it a page while the queue sat behind it.\n * `reviews(status ASC, createdAtMs DESC)` is in\n * cloud/firebase-firestore.indexes.json for exactly this.\n */\n const {\n rows: reviews,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(\n collection(firestore, 'hosts', hostId, 'reviews'),\n ...(filter === 'all' ? [] : [where('status', '==', filter)]),\n orderBy('createdAtMs', 'desc'),\n limit(pageLimit),\n ),\n [firestore, hostId, filter],\n { idField: '$id' },\n )\n const [replyFor, setReplyFor] = useState<string | null>(null)\n const [reply, setReply] = useState('')\n\n // An approved review is on the product page — its star aggregate and its\n // reply — so moderating one carries the site's cache drop (AGL-3386).\n const setStatus = (review: any, status: string) =>\n writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'reviews', review.$id), {\n status,\n }),\n })\n\n return (\n <EntitlementGatedCard\n help={reviewsHelp}\n hostId={hostId}\n feature=\"productReviews\"\n header={'Review moderation'}\n upsell={\n 'Verified-buyer reviews let customers rate what they bought and ' +\n 'show the star aggregate on your product pages and in search ' +\n 'results.'\n }\n >\n <CardDisplay\n header={'Review moderation'}\n help={reviewsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <TextField\n label=\"Show\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n size=\"small\"\n select\n sx={{ maxWidth: 180 }}\n >\n <MenuItem value=\"pending\">{'Pending'}</MenuItem>\n <MenuItem value=\"approved\">{'Approved'}</MenuItem>\n <MenuItem value=\"rejected\">{'Rejected'}</MenuItem>\n <MenuItem value=\"all\">{'All'}</MenuItem>\n </TextField>\n {reviews.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nothing to moderate.'}\n </Typography>\n ) : (\n reviews.map((review: any) => (\n <Stack key={review.$id} spacing={0.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Rating value={review.rating} size=\"small\" readOnly />\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified\" size=\"small\" color=\"success\" variant=\"outlined\" />\n ) : null}\n <Chip label={review.status} size=\"small\" variant=\"outlined\" />\n </Stack>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {review.body}\n </Typography>\n <Stack direction=\"row\" spacing={1}>\n {review.status !== 'approved' ? (\n <Button size=\"small\" onClick={() => setStatus(review, 'approved')}>\n {'Approve'}\n </Button>\n ) : null}\n {review.status !== 'rejected' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => setStatus(review, 'rejected')}\n >\n {'Reject'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={() => {\n setReplyFor(review.$id)\n setReply(review.reply ?? '')\n }}\n >\n {review.reply ? 'Edit reply' : 'Reply'}\n </Button>\n </Stack>\n {replyFor === review.$id ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n value={reply}\n onChange={(event) => setReply(event.target.value)}\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"Public seller reply\"\n />\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'reviews', review.$id),\n { reply: reply.trim().slice(0, 500) || null },\n ),\n })\n setReplyFor(null)\n enqueueSnackbar('Reply saved', {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Save'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={reviews.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nReviewsModerationCard.displayName = 'ReviewsModerationCard'\n\nexport default ReviewsModerationCard\n"],"names":["CardDisplay","ListPagination","useSnackbar","Button","Chip","MenuItem","Rating","Stack","TextField","Typography","collection","doc","limit","orderBy","query","where","useState","useFirestore","useUser","writeSiteWideChange","usePagedCollection","EntitlementGatedCard","pluginDocsHelp","reviewsHelp","anchor","ReviewsModerationCard","props","hostId","firestore","data","user","enqueueSnackbar","filter","setFilter","rows","reviews","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","replyFor","setReplyFor","reply","setReply","setStatus","review","status","write","batch","update","$id","help","feature","header","upsell","contentGutterX","contentGutterY","spacing","label","value","onChange","event","target","size","select","sx","maxWidth","length","variant","color","map","direction","alignItems","rating","readOnly","fontWeight","authorName","verified","body","onClick","flex","placeholder","trim","slice","persist","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/reviews-moderation-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n MenuItem,\n Rating,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n REVIEW_MODERATION_REASONS,\n REVIEW_MODERATION_REASON_LABELS,\n type ReviewModerationReason,\n} from '../../model/product-reviews'\n\nexport interface ReviewsModerationCardProps {\n hostId: string\n}\n\n/**\n * The status a \"Hide\" writes (AGL-3736): a published review is hidden, a\n * pending one is rejected. Either way a content reason goes with it.\n */\nexport function hiddenStatusFor(review: { status?: string }): 'hidden' | 'rejected' {\n return review.status === 'pending' ? 'rejected' : 'hidden'\n}\n\n/**\n * The fields a moderation decision writes. Exactly the keys the Firestore\n * rules let a site editor change on a review: never `verified`, the rating\n * or the text. Approving clears the reason; hiding records it.\n */\nexport function moderationUpdate(\n status: 'approved' | 'hidden' | 'rejected',\n reason: ReviewModerationReason | null,\n nowMs: number,\n): Record<string, unknown> {\n return {\n status,\n moderationReason: status === 'approved' ? null : reason,\n moderatedAtMs: nowMs,\n }\n}\n\n/**\n * Review moderation (AGL-324, AGL-3736): the queue of reviews waiting for\n * approval (unverified ones, under the store's \"anyone\" policy), with\n * approve, hide-for-a-reason and inline seller replies. Verified reviews\n * publish on their own; the merchant may hide one only for a content reason\n * (spam, abuse, off topic, private information) — the FTC's 2024 rule bars\n * suppressing an honest review for being negative — and the reason is kept.\n * Seller replies; approved reviews render on the product-reviews\n * block with the aggregate in JSON-LD.\n *\n * Gated on `productReviews` since AGL-2080. `server/reviews.ts:108` enforces\n * it, and this card rendered identically on Free — an operator could work a\n * moderation queue whose approvals the renderer would never honour.\n */\n\nconst reviewsHelp = pluginDocsHelp('catalog', { anchor: '#review-moderation' })\n\nexport function ReviewsModerationCard(props: ReviewsModerationCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [filter, setFilter] = useState('pending')\n /*\n * The status filter is the QUERY, and the order is the server's.\n *\n * This read `limit(200)` with no `orderBy` and then filtered and sorted in\n * the browser. A limited Firestore query with no ordering returns\n * DOCUMENT-ID order, and reviews are written with `.add()`, so those two\n * hundred were a pseudo-random sample — \"Pending\" showed whichever pending\n * reviews happened to fall in it, which on a busy shop is not the queue and\n * cannot be worked through.\n *\n * Filtering here rather than in the browser is what lets the window shrink:\n * a page of 25 mixed reviews might hold three pending ones, and a card that\n * rendered those three would call it a page while the queue sat behind it.\n * `reviews(status ASC, createdAtMs DESC)` is in\n * cloud/firebase-firestore.indexes.json for exactly this.\n */\n const {\n rows: reviews,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(\n collection(firestore, 'hosts', hostId, 'reviews'),\n ...(filter === 'all' ? [] : [where('status', '==', filter)]),\n orderBy('createdAtMs', 'desc'),\n limit(pageLimit),\n ),\n [firestore, hostId, filter],\n { idField: '$id' },\n )\n const [replyFor, setReplyFor] = useState<string | null>(null)\n const [reply, setReply] = useState('')\n const [hideFor, setHideFor] = useState<string | null>(null)\n const [hideReason, setHideReason] = useState<ReviewModerationReason>('spam')\n\n // An approved review is on the product page — its star aggregate and its\n // reply — so moderating one carries the site's cache drop (AGL-3386).\n const setStatus = (\n review: any,\n status: 'approved' | 'hidden' | 'rejected',\n reason: ReviewModerationReason | null = null,\n ) =>\n writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'reviews', review.$id),\n moderationUpdate(status, reason, Date.now()),\n ),\n })\n\n return (\n <EntitlementGatedCard\n help={reviewsHelp}\n hostId={hostId}\n feature=\"productReviews\"\n header={'Review moderation'}\n upsell={\n 'Verified-buyer reviews let customers rate what they bought and ' +\n 'show the star aggregate on your product pages and in search ' +\n 'results.'\n }\n >\n <CardDisplay\n header={'Review moderation'}\n help={reviewsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <TextField\n label=\"Show\"\n value={filter}\n onChange={(event) => setFilter(event.target.value)}\n size=\"small\"\n select\n sx={{ maxWidth: 180 }}\n >\n <MenuItem value=\"pending\">{'Pending'}</MenuItem>\n <MenuItem value=\"approved\">{'Published'}</MenuItem>\n <MenuItem value=\"hidden\">{'Hidden'}</MenuItem>\n <MenuItem value=\"rejected\">{'Rejected'}</MenuItem>\n <MenuItem value=\"all\">{'All'}</MenuItem>\n </TextField>\n {reviews.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Nothing to moderate.'}\n </Typography>\n ) : (\n reviews.map((review: any) => (\n <Stack key={review.$id} spacing={0.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Rating value={review.rating} size=\"small\" readOnly />\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified\" size=\"small\" color=\"success\" variant=\"outlined\" />\n ) : null}\n <Chip label={review.status} size=\"small\" variant=\"outlined\" />\n </Stack>\n {review.moderationReason &&\n review.moderationReason in REVIEW_MODERATION_REASON_LABELS ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Hidden: ${\n REVIEW_MODERATION_REASON_LABELS[\n review.moderationReason as ReviewModerationReason\n ]\n }`}\n </Typography>\n ) : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {review.body}\n </Typography>\n <Stack direction=\"row\" spacing={1}>\n {review.status !== 'approved' ? (\n <Button size=\"small\" onClick={() => setStatus(review, 'approved')}>\n {'Approve'}\n </Button>\n ) : null}\n {review.status !== 'rejected' && review.status !== 'hidden' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => {\n setHideFor(review.$id)\n setHideReason('spam')\n }}\n >\n {review.status === 'pending' ? 'Reject…' : 'Hide…'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={() => {\n setReplyFor(review.$id)\n setReply(review.reply ?? '')\n }}\n >\n {review.reply ? 'Edit reply' : 'Reply'}\n </Button>\n </Stack>\n {hideFor === review.$id ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <TextField\n label=\"Reason\"\n value={hideReason}\n onChange={(event) =>\n setHideReason(event.target.value as ReviewModerationReason)\n }\n size=\"small\"\n select\n sx={{ minWidth: 220 }}\n helperText=\"Reviews can't be hidden for being negative.\"\n >\n {REVIEW_MODERATION_REASONS.map((reason) => (\n <MenuItem key={reason} value={reason}>\n {REVIEW_MODERATION_REASON_LABELS[reason]}\n </MenuItem>\n ))}\n </TextField>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"error\"\n onClick={async () => {\n await setStatus(review, hiddenStatusFor(review), hideReason)\n setHideFor(null)\n }}\n >\n {review.status === 'pending' ? 'Reject' : 'Hide'}\n </Button>\n <Button size=\"small\" onClick={() => setHideFor(null)}>\n {'Cancel'}\n </Button>\n </Stack>\n ) : null}\n {replyFor === review.$id ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n value={reply}\n onChange={(event) => setReply(event.target.value)}\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"Public seller reply\"\n />\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'reviews', review.$id),\n { reply: reply.trim().slice(0, 500) || null },\n ),\n })\n setReplyFor(null)\n enqueueSnackbar('Reply saved', {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Save'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={reviews.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nReviewsModerationCard.displayName = 'ReviewsModerationCard'\n\nexport default ReviewsModerationCard\n"],"names":["CardDisplay","ListPagination","useSnackbar","Button","Chip","MenuItem","Rating","Stack","TextField","Typography","collection","doc","limit","orderBy","query","where","useState","useFirestore","useUser","writeSiteWideChange","usePagedCollection","EntitlementGatedCard","pluginDocsHelp","REVIEW_MODERATION_REASONS","REVIEW_MODERATION_REASON_LABELS","hiddenStatusFor","review","status","moderationUpdate","reason","nowMs","moderationReason","moderatedAtMs","reviewsHelp","anchor","ReviewsModerationCard","props","hostId","firestore","data","user","enqueueSnackbar","filter","setFilter","rows","reviews","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","replyFor","setReplyFor","reply","setReply","hideFor","setHideFor","hideReason","setHideReason","setStatus","write","batch","update","$id","Date","now","help","feature","header","upsell","contentGutterX","contentGutterY","spacing","label","value","onChange","event","target","size","select","sx","maxWidth","length","variant","color","map","direction","alignItems","rating","readOnly","fontWeight","authorName","verified","body","onClick","minWidth","helperText","flex","placeholder","trim","slice","persist","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,WAAW,QAAQ,uBAAsB;AAClD,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,MAAM,EACNC,IAAI,EACJC,QAAQ,EACRC,MAAM,EACNC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAClF,SAAkBC,QAAQ,QAAQ,QAAO;AACzC,SAASC,YAAY,EAAEC,OAAO,QAAQ,iCAAgC;AACtE,SAASC,mBAAmB,QAAQ,gEAA+D;AACnG,SAASC,kBAAkB,QAAQ,iCAAgC;AACnE,SAASC,oBAAoB,QAAQ,kCAA8B;AACnE,SAASC,cAAc,QAAQ,eAAc;AAC7C,SACEC,yBAAyB,EACzBC,+BAA+B,QAE1B,iCAA6B;AAMpC;;;CAGC,GACD,OAAO,SAASC,gBAAgBC,MAA2B;IACzD,OAAOA,OAAOC,MAAM,KAAK,YAAY,aAAa;AACpD;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBACdD,MAA0C,EAC1CE,MAAqC,EACrCC,KAAa;IAEb,OAAO;QACLH;QACAI,kBAAkBJ,WAAW,aAAa,OAAOE;QACjDG,eAAeF;IACjB;AACF;AAEA;;;;;;;;;;;;;CAaC,GAED,MAAMG,cAAcX,eAAe,WAAW;IAAEY,QAAQ;AAAqB;AAE7E,OAAO,SAASC,sBAAsBC,KAAiC;IACrE,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAME,YAAYrB;IAClB,MAAM,EAAEsB,MAAMC,IAAI,EAAE,GAAGtB;IACvB,MAAM,EAAEuB,eAAe,EAAE,GAAGvC;IAC5B,MAAM,CAACwC,QAAQC,UAAU,GAAG3B,SAAS;IACrC;;;;;;;;;;;;;;;GAeC,GACD,MAAM,EACJ4B,MAAMC,OAAO,EACbC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACZ,GAAG9B,mBACF,CAAC+B,YACCrC,MACEJ,WAAW4B,WAAW,SAASD,QAAQ,eACnCK,WAAW,QAAQ,EAAE,GAAG;YAAC3B,MAAM,UAAU,MAAM2B;SAAQ,EAC3D7B,QAAQ,eAAe,SACvBD,MAAMuC,aAEV;QAACb;QAAWD;QAAQK;KAAO,EAC3B;QAAEU,SAAS;IAAM;IAEnB,MAAM,CAACC,UAAUC,YAAY,GAAGtC,SAAwB;IACxD,MAAM,CAACuC,OAAOC,SAAS,GAAGxC,SAAS;IACnC,MAAM,CAACyC,SAASC,WAAW,GAAG1C,SAAwB;IACtD,MAAM,CAAC2C,YAAYC,cAAc,GAAG5C,SAAiC;IAErE,yEAAyE;IACzE,sEAAsE;IACtE,MAAM6C,YAAY,CAChBnC,QACAC,QACAE,SAAwC,IAAI,GAE5CV,oBAAoB;YAClBmB;YACAE;YACAH;YACAyB,OAAO,CAACC,QACNA,MAAMC,MAAM,CACVrD,IAAI2B,WAAW,SAASD,QAAQ,WAAWX,OAAOuC,GAAG,GACrDrC,iBAAiBD,QAAQE,QAAQqC,KAAKC,GAAG;QAE/C;IAEF,qBACE,KAAC9C;QACC+C,MAAMnC;QACNI,QAAQA;QACRgC,SAAQ;QACRC,QAAQ;QACRC,QACE,oEACA,iEACA;kBAGJ,cAAA,KAACvE;YACCsE,QAAQ;YACRF,MAAMnC;YACNuC,cAAc;YACdC,cAAc;sBAEd,cAAA,MAAClE;gBAAMmE,SAAS;;kCACd,MAAClE;wBACCmE,OAAM;wBACNC,OAAOlC;wBACPmC,UAAU,CAACC,QAAUnC,UAAUmC,MAAMC,MAAM,CAACH,KAAK;wBACjDI,MAAK;wBACLC,MAAM;wBACNC,IAAI;4BAAEC,UAAU;wBAAI;;0CAEpB,KAAC9E;gCAASuE,OAAM;0CAAW;;0CAC3B,KAACvE;gCAASuE,OAAM;0CAAY;;0CAC5B,KAACvE;gCAASuE,OAAM;0CAAU;;0CAC1B,KAACvE;gCAASuE,OAAM;0CAAY;;0CAC5B,KAACvE;gCAASuE,OAAM;0CAAO;;;;oBAExB/B,QAAQuC,MAAM,KAAK,kBAClB,KAAC3E;wBAAW4E,SAAQ;wBAAQC,OAAM;kCAC/B;yBAGHzC,QAAQ0C,GAAG,CAAC,CAAC7D,uBACX,MAACnB;4BAAuBmE,SAAS;;8CAC/B,MAACnE;oCAAMiF,WAAU;oCAAMd,SAAS;oCAAGQ,IAAI;wCAAEO,YAAY;oCAAS;;sDAC5D,KAACnF;4CAAOsE,OAAOlD,OAAOgE,MAAM;4CAAEV,MAAK;4CAAQW,QAAQ;;sDACnD,KAAClF;4CAAW4E,SAAQ;4CAAQH,IAAI;gDAAEU,YAAY;4CAAI;sDAC/ClE,OAAOmE,UAAU;;wCAEnBnE,OAAOoE,QAAQ,iBACd,KAAC1F;4CAAKuE,OAAM;4CAAWK,MAAK;4CAAQM,OAAM;4CAAUD,SAAQ;6CAC1D;sDACJ,KAACjF;4CAAKuE,OAAOjD,OAAOC,MAAM;4CAAEqD,MAAK;4CAAQK,SAAQ;;;;gCAElD3D,OAAOK,gBAAgB,IACxBL,OAAOK,gBAAgB,IAAIP,gDACzB,KAACf;oCAAW4E,SAAQ;oCAAUC,OAAM;8CACjC,CAAC,QAAQ,EACR9D,+BAA+B,CAC7BE,OAAOK,gBAAgB,CACxB,EACD;qCAEF;8CACJ,KAACtB;oCAAW4E,SAAQ;oCAAQC,OAAM;8CAC/B5D,OAAOqE,IAAI;;8CAEd,MAACxF;oCAAMiF,WAAU;oCAAMd,SAAS;;wCAC7BhD,OAAOC,MAAM,KAAK,2BACjB,KAACxB;4CAAO6E,MAAK;4CAAQgB,SAAS,IAAMnC,UAAUnC,QAAQ;sDACnD;6CAED;wCACHA,OAAOC,MAAM,KAAK,cAAcD,OAAOC,MAAM,KAAK,yBACjD,KAACxB;4CACC6E,MAAK;4CACLM,OAAM;4CACNU,SAAS;gDACPtC,WAAWhC,OAAOuC,GAAG;gDACrBL,cAAc;4CAChB;sDAEClC,OAAOC,MAAM,KAAK,YAAY,YAAY;6CAE3C;sDACJ,KAACxB;4CACC6E,MAAK;4CACLgB,SAAS;oDAEEtE;gDADT4B,YAAY5B,OAAOuC,GAAG;gDACtBT,UAAS9B,gBAAAA,OAAO6B,KAAK,YAAZ7B,gBAAgB;4CAC3B;sDAECA,OAAO6B,KAAK,GAAG,eAAe;;;;gCAGlCE,YAAY/B,OAAOuC,GAAG,iBACrB,MAAC1D;oCAAMiF,WAAU;oCAAMd,SAAS;oCAAGQ,IAAI;wCAAEO,YAAY;oCAAS;;sDAC5D,KAACjF;4CACCmE,OAAM;4CACNC,OAAOjB;4CACPkB,UAAU,CAACC,QACTlB,cAAckB,MAAMC,MAAM,CAACH,KAAK;4CAElCI,MAAK;4CACLC,MAAM;4CACNC,IAAI;gDAAEe,UAAU;4CAAI;4CACpBC,YAAW;sDAEV3E,0BAA0BgE,GAAG,CAAC,CAAC1D,uBAC9B,KAACxB;oDAAsBuE,OAAO/C;8DAC3BL,+BAA+B,CAACK,OAAO;mDAD3BA;;sDAKnB,KAAC1B;4CACC6E,MAAK;4CACLK,SAAQ;4CACRC,OAAM;4CACNU,SAAS;gDACP,MAAMnC,UAAUnC,QAAQD,gBAAgBC,SAASiC;gDACjDD,WAAW;4CACb;sDAEChC,OAAOC,MAAM,KAAK,YAAY,WAAW;;sDAE5C,KAACxB;4CAAO6E,MAAK;4CAAQgB,SAAS,IAAMtC,WAAW;sDAC5C;;;qCAGH;gCACHL,aAAa3B,OAAOuC,GAAG,iBACtB,MAAC1D;oCAAMiF,WAAU;oCAAMd,SAAS;;sDAC9B,KAAClE;4CACCoE,OAAOrB;4CACPsB,UAAU,CAACC,QAAUtB,SAASsB,MAAMC,MAAM,CAACH,KAAK;4CAChDI,MAAK;4CACLE,IAAI;gDAAEiB,MAAM;4CAAE;4CACdC,aAAY;;sDAEd,KAACjG;4CACC6E,MAAK;4CACLK,SAAQ;4CACRC,OAAM;4CACNU,SAAS;gDACP,MAAM7E,oBAAoB;oDACxBmB;oDACAE;oDACAH;oDACAyB,OAAO,CAACC,QACNA,MAAMC,MAAM,CACVrD,IAAI2B,WAAW,SAASD,QAAQ,WAAWX,OAAOuC,GAAG,GACrD;4DAAEV,OAAOA,MAAM8C,IAAI,GAAGC,KAAK,CAAC,GAAG,QAAQ;wDAAK;gDAElD;gDACAhD,YAAY;gDACZb,gBAAgB,eAAe;oDAC7B4C,SAAS;oDACTkB,SAAS;gDACX;4CACF;sDAEC;;;qCAGH;;2BAzHM7E,OAAOuC,GAAG;kCA6H1B,KAAChE;wBACC8C,MAAMA;wBACNE,UAAUA;wBACVuD,UAAU3D,QAAQuC,MAAM;wBACxBtC,SAASA;wBACT2D,cAAczD;wBACd0D,kBAAkBxD;;;;;;AAM5B;AACAf,sBAAsBwE,WAAW,GAAG;AAEpC,eAAexE,sBAAqB"}
@@ -34,10 +34,38 @@ export interface ProductReviewsProps {
34
34
  /** Store scope: the heading's level, 2–4 (default 2). */
35
35
  headingLevel?: 2 | 3 | 4 | '2' | '3' | '4';
36
36
  }
37
+ /** What `GET /api/commerce/reviews?mode=eligibility` answers (AGL-3736). */
38
+ export interface ReviewFormEligibility {
39
+ enabled?: boolean;
40
+ policy?: 'verified' | 'anyone';
41
+ signedIn?: boolean;
42
+ verifiedVia?: 'member' | 'token' | null;
43
+ canReview?: boolean;
44
+ needsEmail?: boolean;
45
+ tokenProblem?: string;
46
+ }
47
+ /** Where a shopper signs in to their account on the store. */
48
+ export declare const REVIEW_SIGN_IN_HREF = "/account";
49
+ /** The review link from the review-request email, if the page carries one. */
50
+ export declare function reviewTokenFromLocation(search: string): string;
51
+ /**
52
+ * Which form the block draws, from the server's answer. Pure, so the three
53
+ * states are specified in one place:
54
+ * - `form`: the visitor may review (verified or, under `anyone`, pending);
55
+ * - `sign-in`: the store takes verified reviews only and this visitor is
56
+ * not one — "Only customers who bought this can review." and a sign-in
57
+ * link (no form: a typed email verifies nothing);
58
+ * - `none`: reviews are off on this plan, or the answer has not come yet.
59
+ */
60
+ export declare function reviewFormMode(eligibility: ReviewFormEligibility | null): 'form' | 'sign-in' | 'none';
37
61
  /**
38
62
  * Product reviews block (AGL-324): approved reviews with verified-buyer
39
63
  * badges, seller replies, an aggregate summary (emitted as JSON-LD
40
- * AggregateRating), and a submit form feeding the moderation queue.
64
+ * AggregateRating), and a submit form. Since AGL-3736 the form asks the server
65
+ * who the visitor is first: a verified buyer (signed in, or arriving with the
66
+ * review-request email's link) gets the form and publishes at once; anyone
67
+ * else sees "Only customers who bought this can review" and a sign-in link,
68
+ * unless the store accepts reviews from anyone (held for moderation).
41
69
  */
42
70
  declare const ProductReviews: import("react").ForwardRefExoticComponent<ProductReviewsProps & import("react").RefAttributes<HTMLDivElement>>;
43
71
  export declare const schema: Aglyn.ComponentSchema<ProductReviewsProps>;
@@ -40,6 +40,30 @@ const EDITOR_HINT_SX = {
40
40
  fontSize: 13,
41
41
  fontFamily: 'system-ui, sans-serif'
42
42
  };
43
+ /** Where a shopper signs in to their account on the store. */ export const REVIEW_SIGN_IN_HREF = '/account';
44
+ /** The review link from the review-request email, if the page carries one. */ export function reviewTokenFromLocation(search) {
45
+ var _get;
46
+ const token = (_get = new URLSearchParams(search).get('review')) != null ? _get : '';
47
+ return /^[A-Za-z0-9_-]{32}$/.test(token) ? token : '';
48
+ }
49
+ /**
50
+ * Which form the block draws, from the server's answer. Pure, so the three
51
+ * states are specified in one place:
52
+ * - `form`: the visitor may review (verified or, under `anyone`, pending);
53
+ * - `sign-in`: the store takes verified reviews only and this visitor is
54
+ * not one — "Only customers who bought this can review." and a sign-in
55
+ * link (no form: a typed email verifies nothing);
56
+ * - `none`: reviews are off on this plan, or the answer has not come yet.
57
+ */ export function reviewFormMode(eligibility) {
58
+ if (!eligibility || eligibility.enabled === false) return 'none';
59
+ if (eligibility.canReview) return 'form';
60
+ return 'sign-in';
61
+ }
62
+ const TOKEN_PROBLEM_TEXT = {
63
+ review_token_used: 'This review link has already been used.',
64
+ review_token_expired: 'This review link has expired.',
65
+ review_token_invalid: 'This review link is not valid for this product.'
66
+ };
43
67
  async function resolveProductIdFromSlug(hostId) {
44
68
  var _ref;
45
69
  var _payload_product;
@@ -53,7 +77,11 @@ async function resolveProductIdFromSlug(hostId) {
53
77
  /**
54
78
  * Product reviews block (AGL-324): approved reviews with verified-buyer
55
79
  * badges, seller replies, an aggregate summary (emitted as JSON-LD
56
- * AggregateRating), and a submit form feeding the moderation queue.
80
+ * AggregateRating), and a submit form. Since AGL-3736 the form asks the server
81
+ * who the visitor is first: a verified buyer (signed in, or arriving with the
82
+ * review-request email's link) gets the form and publishes at once; anyone
83
+ * else sees "Only customers who bought this can review" and a sign-in link,
84
+ * unless the store accepts reviews from anyone (held for moderation).
57
85
  */ const ProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
58
86
  if (props.scope === 'store') return /*#__PURE__*/ _jsx(StoreReviews, _extends({
59
87
  ref: ref
@@ -287,6 +315,10 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
287
315
  authorEmail: ''
288
316
  });
289
317
  const [state, setState] = useState('idle');
318
+ const [eligibility, setEligibility] = useState(null);
319
+ const [reviewToken, setReviewToken] = useState('');
320
+ const [submitted, setSubmitted] = useState(null);
321
+ const [errorText, setErrorText] = useState('');
290
322
  useEffect(()=>{
291
323
  if (!hostId) return;
292
324
  let active = true;
@@ -303,6 +335,14 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
303
335
  count: 0,
304
336
  average: 0
305
337
  });
338
+ // May this visitor review, and would it be verified? (AGL-3736)
339
+ const token = reviewTokenFromLocation(window.location.search);
340
+ setReviewToken(token);
341
+ const answer = await fetch(`/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` + `&productId=${encodeURIComponent(resolved)}&mode=eligibility` + (token ? `&review=${encodeURIComponent(token)}` : ''), {
342
+ credentials: 'same-origin'
343
+ }).catch(()=>null);
344
+ if (!active || !(answer == null ? void 0 : answer.ok)) return;
345
+ setEligibility(await answer.json().catch(()=>null));
306
346
  })();
307
347
  return ()=>{
308
348
  active = false;
@@ -322,10 +362,27 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
322
362
  },
323
363
  body: JSON.stringify(_extends({
324
364
  hostId,
325
- productId
326
- }, form))
365
+ productId,
366
+ rating: form.rating,
367
+ body: form.body,
368
+ authorName: form.authorName
369
+ }, (eligibility == null ? void 0 : eligibility.needsEmail) ? {
370
+ authorEmail: form.authorEmail
371
+ } : {}, reviewToken ? {
372
+ reviewToken
373
+ } : {}))
327
374
  });
328
- setState(response.ok ? 'done' : 'error');
375
+ const payload = await response.json().catch(()=>({}));
376
+ if (response.ok) {
377
+ setSubmitted({
378
+ pending: Boolean(payload == null ? void 0 : payload.pending)
379
+ });
380
+ setState('done');
381
+ } else {
382
+ var _ref;
383
+ setErrorText(String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : ''));
384
+ setState('error');
385
+ }
329
386
  } catch (unused) {
330
387
  setState('error');
331
388
  }
@@ -420,10 +477,29 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
420
477
  color: "text.secondary",
421
478
  children: 'No reviews yet — be the first.'
422
479
  }) : null,
480
+ (eligibility == null ? void 0 : eligibility.tokenProblem) && TOKEN_PROBLEM_TEXT[eligibility.tokenProblem] ? /*#__PURE__*/ _jsx(Alert, {
481
+ severity: "info",
482
+ children: TOKEN_PROBLEM_TEXT[eligibility.tokenProblem]
483
+ }) : null,
423
484
  state === 'done' ? /*#__PURE__*/ _jsx(Alert, {
424
485
  severity: "success",
425
- children: 'Thanks — your review is awaiting moderation.'
426
- }) : /*#__PURE__*/ _jsxs(Box, {
486
+ children: (submitted == null ? void 0 : submitted.pending) ? 'Thanks — your review is awaiting moderation.' : 'Thanks — your review is posted.'
487
+ }) : reviewFormMode(eligibility) === 'sign-in' ? /*#__PURE__*/ _jsxs(Typography, {
488
+ variant: "body2",
489
+ color: "text.secondary",
490
+ "data-review-sign-in": "",
491
+ children: [
492
+ 'Only customers who bought this can review. ',
493
+ /*#__PURE__*/ _jsx(Box, {
494
+ component: "a",
495
+ href: REVIEW_SIGN_IN_HREF,
496
+ sx: {
497
+ color: 'primary.main'
498
+ },
499
+ children: 'Sign in'
500
+ })
501
+ ]
502
+ }) : reviewFormMode(eligibility) === 'none' ? null : /*#__PURE__*/ _jsxs(Box, {
427
503
  sx: {
428
504
  display: 'flex',
429
505
  flexDirection: 'column',
@@ -435,6 +511,15 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
435
511
  variant: "subtitle2",
436
512
  children: 'Write a review'
437
513
  }),
514
+ (eligibility == null ? void 0 : eligibility.verifiedVia) ? /*#__PURE__*/ _jsx(Chip, {
515
+ label: "Your review will show as Verified buyer",
516
+ size: "small",
517
+ variant: "outlined",
518
+ color: "success",
519
+ sx: {
520
+ alignSelf: 'flex-start'
521
+ }
522
+ }) : null,
438
523
  /*#__PURE__*/ _jsx(Rating, {
439
524
  value: form.rating,
440
525
  onChange: (_event, value)=>setForm((prev)=>_extends({}, prev, {
@@ -468,8 +553,8 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
468
553
  flex: 1
469
554
  }
470
555
  }),
471
- /*#__PURE__*/ _jsx(TextField, {
472
- placeholder: "Email (verifies your purchase)",
556
+ (eligibility == null ? void 0 : eligibility.needsEmail) ? /*#__PURE__*/ _jsx(TextField, {
557
+ placeholder: "Email (never shown)",
473
558
  type: "email",
474
559
  value: form.authorEmail,
475
560
  onChange: (event)=>setForm((prev)=>_extends({}, prev, {
@@ -479,17 +564,17 @@ const SingleProductReviews = /*#__PURE__*/ forwardRef((props, ref)=>{
479
564
  sx: {
480
565
  flex: 1.4
481
566
  }
482
- })
567
+ }) : null
483
568
  ]
484
569
  }),
485
570
  state === 'error' ? /*#__PURE__*/ _jsx(Alert, {
486
571
  severity: "error",
487
- children: 'Could not submit — check the fields.'
572
+ children: errorText || 'Could not submit — check the fields.'
488
573
  }) : null,
489
574
  /*#__PURE__*/ _jsx(Button, {
490
575
  variant: "outlined",
491
576
  size: "small",
492
- disabled: state === 'busy' || !form.body.trim() || !form.authorEmail.trim(),
577
+ disabled: state === 'busy' || !form.body.trim() || Boolean((eligibility == null ? void 0 : eligibility.needsEmail) && !form.authorEmail.trim()),
493
578
  onClick: handleSubmit,
494
579
  sx: {
495
580
  alignSelf: 'flex-start'
@@ -506,7 +591,7 @@ export const schema = {
506
591
  $id: ID,
507
592
  pluginId: BUNDLE_ID,
508
593
  displayName: 'Product reviews',
509
- description: 'Approved reviews with verified-buyer badges, and a form to add one.',
594
+ description: 'Approved reviews with verified-buyer badges, and a form for customers ' + 'who bought the product to add one.',
510
595
  category: Aglyn.ComponentCategory.COMMERCE,
511
596
  icon: {
512
597
  path: mdiStarOutline.path,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/product-reviews.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiStarOutline } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Chip from '@mui/material/Chip'\nimport Divider from '@mui/material/Divider'\nimport Rating from '@mui/material/Rating'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useEffect, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'product-reviews'\n\nexport interface ProductReviewsProps {\n /** Product id; blank resolves from the /products/{slug} page product. */\n productId?: string\n heading?: string\n /**\n * `product` (default): one product's reviews with the submit form.\n * `store`: the store's latest approved reviews across every product, with\n * the average and count, and no form — for a storefront home. In store\n * scope the block draws its own heading (`heading`, default \"What\n * customers say\") and draws NOTHING when there are no approved reviews,\n * so a page never shows an empty reviews band.\n */\n scope?: 'product' | 'store'\n /** Store scope: how many reviews to show (default 6, at most 12). */\n maxItems?: number\n /** Store scope: the heading's level, 2–4 (default 2). */\n headingLevel?: 2 | 3 | 4 | '2' | '3' | '4'\n}\n\ninterface StoreReviewView {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n createdAtMs: number\n productName?: string\n productSlug?: string\n}\n\nconst EDITOR_HINT_SX = {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n} as const\n\ninterface ReviewView {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply?: string | null\n createdAtMs: number\n}\n\nasync function resolveProductIdFromSlug(hostId: string): Promise<string> {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/)\n if (!match) return ''\n const response = await fetch(\n `/api/commerce/product?hostId=${encodeURIComponent(hostId)}` +\n `&slug=${encodeURIComponent(decodeURIComponent(match[1]))}`,\n ).catch(() => null)\n if (!response?.ok) return ''\n const payload = await response.json().catch(() => ({}))\n return String(payload?.product?.id ?? '')\n}\n\n/**\n * Product reviews block (AGL-324): approved reviews with verified-buyer\n * badges, seller replies, an aggregate summary (emitted as JSON-LD\n * AggregateRating), and a submit form feeding the moderation queue.\n */\nconst ProductReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n if (props.scope === 'store') return <StoreReviews ref={ref} {...props} />\n return <SingleProductReviews ref={ref} {...props} />\n },\n)\nProductReviews.displayName = 'AglynProductReviews'\n\n/**\n * The store-wide reviews band (`scope: 'store'`): the store's latest APPROVED\n * reviews, read-only stars, the reviewer's first name and initial, a verified\n * badge, and the product reviewed. Real reviews or nothing: with none it\n * renders an empty, zero-height element — never sample reviews on a live page\n * (sample testimonials presented as customers' would be fake reviews). The\n * editor, which has no site, gets a dashed hint instead.\n */\nconst StoreReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n const {\n productId: _productId,\n heading,\n scope: _scope,\n maxItems,\n headingLevel,\n ...rest\n } = props\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const [payload, setPayload] = useState<{\n reviews: StoreReviewView[]\n aggregate: { count: number; average: number }\n } | null>(null)\n const limit = maxItems && Number(maxItems) > 0 ? Math.min(12, Math.floor(Number(maxItems))) : 6\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(\n `/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` +\n `&scope=store&limit=${limit}`,\n )\n .then((response) => (response.ok ? response.json() : null))\n .then((body) => {\n if (!active || !body) return\n setPayload({\n reviews: Array.isArray(body.reviews) ? body.reviews : [],\n aggregate: body.aggregate ?? { count: 0, average: 0 },\n })\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [hostId, limit])\n\n if (!hostId) {\n return (\n <Box ref={ref} {...rest} sx={[EDITOR_HINT_SX, ...nodeSx]}>\n {'★★★★★ Customer reviews appear here once shoppers review your products'}\n </Box>\n )\n }\n // Loading, failed, or none approved: nothing at all — not the node's\n // padding, not a heading over an empty band.\n if (!payload || payload.reviews.length === 0) {\n return <Box ref={ref} data-store-reviews-empty=\"\" sx={{ display: 'none' }} />\n }\n // A select may store the level as a string; read it as a number.\n const asked = Number(headingLevel)\n const level = asked === 3 || asked === 4 ? asked : 2\n const { aggregate } = payload\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[{ display: 'flex', flexDirection: 'column', gap: 3 }, ...nodeSx]}\n >\n <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, textAlign: 'center' }}>\n <Typography variant={level === 2 ? 'h4' : level === 3 ? 'h5' : 'h6'} component={`h${level}`}>\n {heading || 'What customers say'}\n </Typography>\n {aggregate.count > 0 ? (\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>\n <Rating value={aggregate.average} precision={0.1} readOnly size=\"small\" />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${aggregate.average} out of 5 · ${aggregate.count} ${aggregate.count === 1 ? 'review' : 'reviews'}`}\n </Typography>\n </Box>\n ) : null}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gap: 2,\n gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },\n }}\n >\n {payload.reviews.map((review) => (\n <Box\n key={review.id}\n component=\"figure\"\n sx={{\n m: 0,\n p: 2.5,\n display: 'flex',\n flexDirection: 'column',\n gap: 1,\n border: 1,\n borderColor: 'divider',\n borderRadius: 2,\n bgcolor: 'background.paper',\n }}\n >\n <Rating value={review.rating} readOnly size=\"small\" />\n <Typography component=\"blockquote\" variant=\"body1\" sx={{ m: 0, flex: 1 }}>\n {review.body}\n </Typography>\n <Box component=\"figcaption\" sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified buyer\" size=\"small\" variant=\"outlined\" color=\"success\" />\n ) : null}\n </Box>\n {review.productName ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component={review.productSlug ? 'a' : 'span'}\n {...(review.productSlug ? { href: `/products/${review.productSlug}` } : {})}\n sx={{ textDecoration: 'none' }}\n >\n {review.productName}\n </Typography>\n ) : null}\n </Box>\n ))}\n </Box>\n </Box>\n )\n },\n)\nStoreReviews.displayName = 'AglynStoreReviews'\n\nconst SingleProductReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n const {\n productId: productIdProp,\n heading,\n scope: _scope,\n maxItems: _maxItems,\n headingLevel: _headingLevel,\n ...rest\n } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n const [productId, setProductId] = useState(productIdProp ?? '')\n const [reviews, setReviews] = useState<ReviewView[] | null>(null)\n const [aggregate, setAggregate] = useState({ count: 0, average: 0 })\n const [form, setForm] = useState({\n rating: 5,\n body: '',\n authorName: '',\n authorEmail: '',\n })\n const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>(\n 'idle',\n )\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void (async () => {\n const resolved =\n productIdProp || (await resolveProductIdFromSlug(hostId))\n if (!active || !resolved) return\n setProductId(resolved)\n const response = await fetch(\n `/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` +\n `&productId=${encodeURIComponent(resolved)}`,\n ).catch(() => null)\n if (!active || !response?.ok) return\n const payload = await response.json().catch(() => ({}))\n setReviews(payload?.reviews ?? [])\n setAggregate(payload?.aggregate ?? { count: 0, average: 0 })\n })()\n return () => {\n active = false\n }\n }, [hostId, productIdProp])\n\n const handleSubmit = async () => {\n if (!hostId || !productId || state === 'busy') return\n setState('busy')\n try {\n const response = await siteFetch('/api/commerce/reviews', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, productId, ...form }),\n })\n setState(response.ok ? 'done' : 'error')\n } catch {\n setState('error')\n }\n }\n\n if (!hostId) {\n return (\n <Box ref={ref} {...rest} sx={[EDITOR_HINT_SX, ...nodeSx]}>\n {'★★★★★ Product reviews render here'}\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest} sx={[{ display: 'flex', flexDirection: 'column', gap: 1.5 }, ...nodeSx]}>\n {/* No JSON-LD here any more (AGL-686). This block used to emit a\n free-standing `AggregateRating` node, which schema.org ignores —\n a rating has to be a PROPERTY of the Product it rates. The PDP's\n server-rendered Product node now carries `aggregateRating`,\n built from the same `readProductReviews` reader this block's API\n uses, so the structured data and the visible stars cannot\n disagree. It also removes the one call site that was primed to\n become stored XSS the moment a reviewer name went into it. */}\n <Typography variant=\"h6\">\n {heading || 'Reviews'}\n {aggregate.count > 0 ? ` (${aggregate.average} ★ · ${aggregate.count})` : ''}\n </Typography>\n {(reviews ?? []).map((review) => (\n <Box key={review.id}>\n <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>\n <Rating value={review.rating} size=\"small\" readOnly />\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified buyer\" size=\"small\" variant=\"outlined\" color=\"success\" />\n ) : null}\n </Box>\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n {review.body}\n </Typography>\n {review.reply ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', pl: 2, mt: 0.5, borderLeft: 2, borderColor: 'divider' }}\n >\n {`Seller: ${review.reply}`}\n </Typography>\n ) : null}\n <Divider sx={{ mt: 1.5 }} />\n </Box>\n ))}\n {reviews && reviews.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No reviews yet — be the first.'}\n </Typography>\n ) : null}\n {state === 'done' ? (\n <Alert severity=\"success\">\n {'Thanks — your review is awaiting moderation.'}\n </Alert>\n ) : (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxWidth: 480 }}>\n <Typography variant=\"subtitle2\">{'Write a review'}</Typography>\n <Rating\n value={form.rating}\n onChange={(_event, value) =>\n setForm((prev) => ({ ...prev, rating: value ?? 5 }))\n }\n />\n <TextField\n placeholder=\"What did you think?\"\n value={form.body}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, body: event.target.value }))\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Box sx={{ display: 'flex', gap: 1 }}>\n <TextField\n placeholder=\"Name\"\n value={form.authorName}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, authorName: event.target.value }))\n }\n size=\"small\"\n sx={{ flex: 1 }}\n />\n <TextField\n placeholder=\"Email (verifies your purchase)\"\n type=\"email\"\n value={form.authorEmail}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, authorEmail: event.target.value }))\n }\n size=\"small\"\n sx={{ flex: 1.4 }}\n />\n </Box>\n {state === 'error' ? (\n <Alert severity=\"error\">{'Could not submit — check the fields.'}</Alert>\n ) : null}\n <Button\n variant=\"outlined\"\n size=\"small\"\n disabled={state === 'busy' || !form.body.trim() || !form.authorEmail.trim()}\n onClick={handleSubmit}\n sx={{ alignSelf: 'flex-start' }}\n >\n {'Submit review'}\n </Button>\n </Box>\n )}\n </Box>\n )\n },\n)\nSingleProductReviews.displayName = 'AglynSingleProductReviews'\n\nexport const schema: Aglyn.ComponentSchema<ProductReviewsProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Product reviews',\n description:\n 'Approved reviews with verified-buyer badges, and a form to add one.',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiStarOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'productId',\n label: 'Product id',\n description:\n 'Whose reviews these are. Blank reads the product from the URL, ' +\n 'which is what you want on a product template; naming one pins the ' +\n 'block to that product wherever it is placed.',\n component: Aglyn.FieldComponentType.PRODUCT_SELECT,\n },\n {\n name: 'heading',\n label: 'Heading',\n description: 'Defaults to \"Reviews\" (\"What customers say\" store-wide).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n // Default `product`, so a page that already places this block keeps it.\n name: 'scope',\n label: 'Show',\n description:\n 'One product’s reviews with a form, or the store’s latest approved ' +\n 'reviews across every product. Store-wide shows nothing until a ' +\n 'shopper’s review is approved.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { label: 'This product', value: 'product' },\n { label: 'Whole store', value: 'store' },\n ],\n },\n {\n name: 'maxItems',\n label: 'Max reviews',\n description: 'Store-wide: how many to show (default 6, at most 12).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'headingLevel',\n label: 'Heading level',\n description: 'Store-wide: the heading’s level on the page (default H2).',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { label: 'H2', value: '2' },\n { label: 'H3', value: '3' },\n { label: 'H4', value: '4' },\n ],\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Product reviews',\n pluginId: BUNDLE_ID,\n description: 'Verified-buyer reviews with a submit form',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiStarOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default ProductReviews\n"],"names":["Aglyn","mdiStarOutline","Alert","Box","Button","Chip","Divider","Rating","TextField","Typography","forwardRef","useEffect","useState","BUNDLE_ID","generatePresetId","ID","EDITOR_HINT_SX","p","border","borderColor","borderRadius","color","fontSize","fontFamily","resolveProductIdFromSlug","hostId","payload","match","window","location","pathname","response","fetch","encodeURIComponent","decodeURIComponent","catch","ok","json","String","product","id","ProductReviews","props","ref","scope","StoreReviews","SingleProductReviews","displayName","productId","_productId","heading","_scope","maxItems","headingLevel","rest","nodeSx","Array","isArray","useSite","setPayload","limit","Number","Math","min","floor","active","then","body","reviews","aggregate","count","average","undefined","sx","length","data-store-reviews-empty","display","asked","level","flexDirection","gap","alignItems","textAlign","variant","component","value","precision","readOnly","size","gridTemplateColumns","xs","sm","md","map","review","m","bgcolor","rating","flex","flexWrap","fontWeight","authorName","verified","label","productName","productSlug","href","textDecoration","productIdProp","_maxItems","_headingLevel","siteFetch","useSiteFetch","setProductId","setReviews","setAggregate","form","setForm","authorEmail","state","setState","resolved","handleSubmit","method","headers","JSON","stringify","mt","reply","pl","borderLeft","severity","maxWidth","onChange","_event","prev","placeholder","event","target","multiline","minRows","type","disabled","trim","onClick","alignSelf","schema","$id","pluginId","description","category","ComponentCategory","COMMERCE","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","FieldComponentType","PRODUCT_SELECT","TEXT_FIELD","SELECT","options","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/product-reviews.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiStarOutline } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Chip from '@mui/material/Chip'\nimport Divider from '@mui/material/Divider'\nimport Rating from '@mui/material/Rating'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useEffect, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'product-reviews'\n\nexport interface ProductReviewsProps {\n /** Product id; blank resolves from the /products/{slug} page product. */\n productId?: string\n heading?: string\n /**\n * `product` (default): one product's reviews with the submit form.\n * `store`: the store's latest approved reviews across every product, with\n * the average and count, and no form — for a storefront home. In store\n * scope the block draws its own heading (`heading`, default \"What\n * customers say\") and draws NOTHING when there are no approved reviews,\n * so a page never shows an empty reviews band.\n */\n scope?: 'product' | 'store'\n /** Store scope: how many reviews to show (default 6, at most 12). */\n maxItems?: number\n /** Store scope: the heading's level, 2–4 (default 2). */\n headingLevel?: 2 | 3 | 4 | '2' | '3' | '4'\n}\n\ninterface StoreReviewView {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n createdAtMs: number\n productName?: string\n productSlug?: string\n}\n\nconst EDITOR_HINT_SX = {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n} as const\n\ninterface ReviewView {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply?: string | null\n createdAtMs: number\n}\n\n/** What `GET /api/commerce/reviews?mode=eligibility` answers (AGL-3736). */\nexport interface ReviewFormEligibility {\n enabled?: boolean\n policy?: 'verified' | 'anyone'\n signedIn?: boolean\n verifiedVia?: 'member' | 'token' | null\n canReview?: boolean\n needsEmail?: boolean\n tokenProblem?: string\n}\n\n/** Where a shopper signs in to their account on the store. */\nexport const REVIEW_SIGN_IN_HREF = '/account'\n\n/** The review link from the review-request email, if the page carries one. */\nexport function reviewTokenFromLocation(search: string): string {\n const token = new URLSearchParams(search).get('review') ?? ''\n return /^[A-Za-z0-9_-]{32}$/.test(token) ? token : ''\n}\n\n/**\n * Which form the block draws, from the server's answer. Pure, so the three\n * states are specified in one place:\n * - `form`: the visitor may review (verified or, under `anyone`, pending);\n * - `sign-in`: the store takes verified reviews only and this visitor is\n * not one — \"Only customers who bought this can review.\" and a sign-in\n * link (no form: a typed email verifies nothing);\n * - `none`: reviews are off on this plan, or the answer has not come yet.\n */\nexport function reviewFormMode(\n eligibility: ReviewFormEligibility | null,\n): 'form' | 'sign-in' | 'none' {\n if (!eligibility || eligibility.enabled === false) return 'none'\n if (eligibility.canReview) return 'form'\n return 'sign-in'\n}\n\nconst TOKEN_PROBLEM_TEXT: Record<string, string> = {\n review_token_used: 'This review link has already been used.',\n review_token_expired: 'This review link has expired.',\n review_token_invalid: 'This review link is not valid for this product.',\n}\n\nasync function resolveProductIdFromSlug(hostId: string): Promise<string> {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/)\n if (!match) return ''\n const response = await fetch(\n `/api/commerce/product?hostId=${encodeURIComponent(hostId)}` +\n `&slug=${encodeURIComponent(decodeURIComponent(match[1]))}`,\n ).catch(() => null)\n if (!response?.ok) return ''\n const payload = await response.json().catch(() => ({}))\n return String(payload?.product?.id ?? '')\n}\n\n/**\n * Product reviews block (AGL-324): approved reviews with verified-buyer\n * badges, seller replies, an aggregate summary (emitted as JSON-LD\n * AggregateRating), and a submit form. Since AGL-3736 the form asks the server\n * who the visitor is first: a verified buyer (signed in, or arriving with the\n * review-request email's link) gets the form and publishes at once; anyone\n * else sees \"Only customers who bought this can review\" and a sign-in link,\n * unless the store accepts reviews from anyone (held for moderation).\n */\nconst ProductReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n if (props.scope === 'store') return <StoreReviews ref={ref} {...props} />\n return <SingleProductReviews ref={ref} {...props} />\n },\n)\nProductReviews.displayName = 'AglynProductReviews'\n\n/**\n * The store-wide reviews band (`scope: 'store'`): the store's latest APPROVED\n * reviews, read-only stars, the reviewer's first name and initial, a verified\n * badge, and the product reviewed. Real reviews or nothing: with none it\n * renders an empty, zero-height element — never sample reviews on a live page\n * (sample testimonials presented as customers' would be fake reviews). The\n * editor, which has no site, gets a dashed hint instead.\n */\nconst StoreReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n const {\n productId: _productId,\n heading,\n scope: _scope,\n maxItems,\n headingLevel,\n ...rest\n } = props\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const [payload, setPayload] = useState<{\n reviews: StoreReviewView[]\n aggregate: { count: number; average: number }\n } | null>(null)\n const limit = maxItems && Number(maxItems) > 0 ? Math.min(12, Math.floor(Number(maxItems))) : 6\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(\n `/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` +\n `&scope=store&limit=${limit}`,\n )\n .then((response) => (response.ok ? response.json() : null))\n .then((body) => {\n if (!active || !body) return\n setPayload({\n reviews: Array.isArray(body.reviews) ? body.reviews : [],\n aggregate: body.aggregate ?? { count: 0, average: 0 },\n })\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [hostId, limit])\n\n if (!hostId) {\n return (\n <Box ref={ref} {...rest} sx={[EDITOR_HINT_SX, ...nodeSx]}>\n {'★★★★★ Customer reviews appear here once shoppers review your products'}\n </Box>\n )\n }\n // Loading, failed, or none approved: nothing at all — not the node's\n // padding, not a heading over an empty band.\n if (!payload || payload.reviews.length === 0) {\n return <Box ref={ref} data-store-reviews-empty=\"\" sx={{ display: 'none' }} />\n }\n // A select may store the level as a string; read it as a number.\n const asked = Number(headingLevel)\n const level = asked === 3 || asked === 4 ? asked : 2\n const { aggregate } = payload\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[{ display: 'flex', flexDirection: 'column', gap: 3 }, ...nodeSx]}\n >\n <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, textAlign: 'center' }}>\n <Typography variant={level === 2 ? 'h4' : level === 3 ? 'h5' : 'h6'} component={`h${level}`}>\n {heading || 'What customers say'}\n </Typography>\n {aggregate.count > 0 ? (\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>\n <Rating value={aggregate.average} precision={0.1} readOnly size=\"small\" />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${aggregate.average} out of 5 · ${aggregate.count} ${aggregate.count === 1 ? 'review' : 'reviews'}`}\n </Typography>\n </Box>\n ) : null}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gap: 2,\n gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },\n }}\n >\n {payload.reviews.map((review) => (\n <Box\n key={review.id}\n component=\"figure\"\n sx={{\n m: 0,\n p: 2.5,\n display: 'flex',\n flexDirection: 'column',\n gap: 1,\n border: 1,\n borderColor: 'divider',\n borderRadius: 2,\n bgcolor: 'background.paper',\n }}\n >\n <Rating value={review.rating} readOnly size=\"small\" />\n <Typography component=\"blockquote\" variant=\"body1\" sx={{ m: 0, flex: 1 }}>\n {review.body}\n </Typography>\n <Box component=\"figcaption\" sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified buyer\" size=\"small\" variant=\"outlined\" color=\"success\" />\n ) : null}\n </Box>\n {review.productName ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component={review.productSlug ? 'a' : 'span'}\n {...(review.productSlug ? { href: `/products/${review.productSlug}` } : {})}\n sx={{ textDecoration: 'none' }}\n >\n {review.productName}\n </Typography>\n ) : null}\n </Box>\n ))}\n </Box>\n </Box>\n )\n },\n)\nStoreReviews.displayName = 'AglynStoreReviews'\n\nconst SingleProductReviews = forwardRef<HTMLDivElement, ProductReviewsProps>(\n (props, ref) => {\n const {\n productId: productIdProp,\n heading,\n scope: _scope,\n maxItems: _maxItems,\n headingLevel: _headingLevel,\n ...rest\n } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n const [productId, setProductId] = useState(productIdProp ?? '')\n const [reviews, setReviews] = useState<ReviewView[] | null>(null)\n const [aggregate, setAggregate] = useState({ count: 0, average: 0 })\n const [form, setForm] = useState({\n rating: 5,\n body: '',\n authorName: '',\n authorEmail: '',\n })\n const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>(\n 'idle',\n )\n const [eligibility, setEligibility] = useState<ReviewFormEligibility | null>(null)\n const [reviewToken, setReviewToken] = useState('')\n const [submitted, setSubmitted] = useState<{ pending: boolean } | null>(null)\n const [errorText, setErrorText] = useState('')\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void (async () => {\n const resolved =\n productIdProp || (await resolveProductIdFromSlug(hostId))\n if (!active || !resolved) return\n setProductId(resolved)\n const response = await fetch(\n `/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` +\n `&productId=${encodeURIComponent(resolved)}`,\n ).catch(() => null)\n if (!active || !response?.ok) return\n const payload = await response.json().catch(() => ({}))\n setReviews(payload?.reviews ?? [])\n setAggregate(payload?.aggregate ?? { count: 0, average: 0 })\n // May this visitor review, and would it be verified? (AGL-3736)\n const token = reviewTokenFromLocation(window.location.search)\n setReviewToken(token)\n const answer = await fetch(\n `/api/commerce/reviews?hostId=${encodeURIComponent(hostId)}` +\n `&productId=${encodeURIComponent(resolved)}&mode=eligibility` +\n (token ? `&review=${encodeURIComponent(token)}` : ''),\n { credentials: 'same-origin' },\n ).catch(() => null)\n if (!active || !answer?.ok) return\n setEligibility(await answer.json().catch(() => null))\n })()\n return () => {\n active = false\n }\n }, [hostId, productIdProp])\n\n const handleSubmit = async () => {\n if (!hostId || !productId || state === 'busy') return\n setState('busy')\n try {\n const response = await siteFetch('/api/commerce/reviews', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n productId,\n rating: form.rating,\n body: form.body,\n authorName: form.authorName,\n ...(eligibility?.needsEmail ? { authorEmail: form.authorEmail } : {}),\n ...(reviewToken ? { reviewToken } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n setSubmitted({ pending: Boolean(payload?.pending) })\n setState('done')\n } else {\n setErrorText(String(payload?.error ?? ''))\n setState('error')\n }\n } catch {\n setState('error')\n }\n }\n\n if (!hostId) {\n return (\n <Box ref={ref} {...rest} sx={[EDITOR_HINT_SX, ...nodeSx]}>\n {'★★★★★ Product reviews render here'}\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest} sx={[{ display: 'flex', flexDirection: 'column', gap: 1.5 }, ...nodeSx]}>\n {/* No JSON-LD here any more (AGL-686). This block used to emit a\n free-standing `AggregateRating` node, which schema.org ignores —\n a rating has to be a PROPERTY of the Product it rates. The PDP's\n server-rendered Product node now carries `aggregateRating`,\n built from the same `readProductReviews` reader this block's API\n uses, so the structured data and the visible stars cannot\n disagree. It also removes the one call site that was primed to\n become stored XSS the moment a reviewer name went into it. */}\n <Typography variant=\"h6\">\n {heading || 'Reviews'}\n {aggregate.count > 0 ? ` (${aggregate.average} ★ · ${aggregate.count})` : ''}\n </Typography>\n {(reviews ?? []).map((review) => (\n <Box key={review.id}>\n <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>\n <Rating value={review.rating} size=\"small\" readOnly />\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {review.authorName}\n </Typography>\n {review.verified ? (\n <Chip label=\"Verified buyer\" size=\"small\" variant=\"outlined\" color=\"success\" />\n ) : null}\n </Box>\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n {review.body}\n </Typography>\n {review.reply ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', pl: 2, mt: 0.5, borderLeft: 2, borderColor: 'divider' }}\n >\n {`Seller: ${review.reply}`}\n </Typography>\n ) : null}\n <Divider sx={{ mt: 1.5 }} />\n </Box>\n ))}\n {reviews && reviews.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No reviews yet — be the first.'}\n </Typography>\n ) : null}\n {eligibility?.tokenProblem && TOKEN_PROBLEM_TEXT[eligibility.tokenProblem] ? (\n <Alert severity=\"info\">{TOKEN_PROBLEM_TEXT[eligibility.tokenProblem]}</Alert>\n ) : null}\n {state === 'done' ? (\n <Alert severity=\"success\">\n {submitted?.pending\n ? 'Thanks — your review is awaiting moderation.'\n : 'Thanks — your review is posted.'}\n </Alert>\n ) : reviewFormMode(eligibility) === 'sign-in' ? (\n <Typography variant=\"body2\" color=\"text.secondary\" data-review-sign-in=\"\">\n {'Only customers who bought this can review. '}\n <Box component=\"a\" href={REVIEW_SIGN_IN_HREF} sx={{ color: 'primary.main' }}>\n {'Sign in'}\n </Box>\n </Typography>\n ) : reviewFormMode(eligibility) === 'none' ? null : (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxWidth: 480 }}>\n <Typography variant=\"subtitle2\">{'Write a review'}</Typography>\n {eligibility?.verifiedVia ? (\n <Chip\n label=\"Your review will show as Verified buyer\"\n size=\"small\"\n variant=\"outlined\"\n color=\"success\"\n sx={{ alignSelf: 'flex-start' }}\n />\n ) : null}\n <Rating\n value={form.rating}\n onChange={(_event, value) =>\n setForm((prev) => ({ ...prev, rating: value ?? 5 }))\n }\n />\n <TextField\n placeholder=\"What did you think?\"\n value={form.body}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, body: event.target.value }))\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Box sx={{ display: 'flex', gap: 1 }}>\n <TextField\n placeholder=\"Name\"\n value={form.authorName}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, authorName: event.target.value }))\n }\n size=\"small\"\n sx={{ flex: 1 }}\n />\n {eligibility?.needsEmail ? (\n <TextField\n placeholder=\"Email (never shown)\"\n type=\"email\"\n value={form.authorEmail}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, authorEmail: event.target.value }))\n }\n size=\"small\"\n sx={{ flex: 1.4 }}\n />\n ) : null}\n </Box>\n {state === 'error' ? (\n <Alert severity=\"error\">\n {errorText || 'Could not submit — check the fields.'}\n </Alert>\n ) : null}\n <Button\n variant=\"outlined\"\n size=\"small\"\n disabled={\n state === 'busy' ||\n !form.body.trim() ||\n Boolean(eligibility?.needsEmail && !form.authorEmail.trim())\n }\n onClick={handleSubmit}\n sx={{ alignSelf: 'flex-start' }}\n >\n {'Submit review'}\n </Button>\n </Box>\n )}\n </Box>\n )\n },\n)\nSingleProductReviews.displayName = 'AglynSingleProductReviews'\n\nexport const schema: Aglyn.ComponentSchema<ProductReviewsProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Product reviews',\n description:\n 'Approved reviews with verified-buyer badges, and a form for customers ' +\n 'who bought the product to add one.',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiStarOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'productId',\n label: 'Product id',\n description:\n 'Whose reviews these are. Blank reads the product from the URL, ' +\n 'which is what you want on a product template; naming one pins the ' +\n 'block to that product wherever it is placed.',\n component: Aglyn.FieldComponentType.PRODUCT_SELECT,\n },\n {\n name: 'heading',\n label: 'Heading',\n description: 'Defaults to \"Reviews\" (\"What customers say\" store-wide).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n // Default `product`, so a page that already places this block keeps it.\n name: 'scope',\n label: 'Show',\n description:\n 'One product’s reviews with a form, or the store’s latest approved ' +\n 'reviews across every product. Store-wide shows nothing until a ' +\n 'shopper’s review is approved.',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { label: 'This product', value: 'product' },\n { label: 'Whole store', value: 'store' },\n ],\n },\n {\n name: 'maxItems',\n label: 'Max reviews',\n description: 'Store-wide: how many to show (default 6, at most 12).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n {\n name: 'headingLevel',\n label: 'Heading level',\n description: 'Store-wide: the heading’s level on the page (default H2).',\n component: Aglyn.FieldComponentType.SELECT,\n options: [\n { label: 'H2', value: '2' },\n { label: 'H3', value: '3' },\n { label: 'H4', value: '4' },\n ],\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Product reviews',\n pluginId: BUNDLE_ID,\n description: 'Verified-buyer reviews with a submit form',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiStarOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default ProductReviews\n"],"names":["Aglyn","mdiStarOutline","Alert","Box","Button","Chip","Divider","Rating","TextField","Typography","forwardRef","useEffect","useState","BUNDLE_ID","generatePresetId","ID","EDITOR_HINT_SX","p","border","borderColor","borderRadius","color","fontSize","fontFamily","REVIEW_SIGN_IN_HREF","reviewTokenFromLocation","search","token","URLSearchParams","get","test","reviewFormMode","eligibility","enabled","canReview","TOKEN_PROBLEM_TEXT","review_token_used","review_token_expired","review_token_invalid","resolveProductIdFromSlug","hostId","payload","match","window","location","pathname","response","fetch","encodeURIComponent","decodeURIComponent","catch","ok","json","String","product","id","ProductReviews","props","ref","scope","StoreReviews","SingleProductReviews","displayName","productId","_productId","heading","_scope","maxItems","headingLevel","rest","nodeSx","Array","isArray","useSite","setPayload","limit","Number","Math","min","floor","active","then","body","reviews","aggregate","count","average","undefined","sx","length","data-store-reviews-empty","display","asked","level","flexDirection","gap","alignItems","textAlign","variant","component","value","precision","readOnly","size","gridTemplateColumns","xs","sm","md","map","review","m","bgcolor","rating","flex","flexWrap","fontWeight","authorName","verified","label","productName","productSlug","href","textDecoration","productIdProp","_maxItems","_headingLevel","siteFetch","useSiteFetch","setProductId","setReviews","setAggregate","form","setForm","authorEmail","state","setState","setEligibility","reviewToken","setReviewToken","submitted","setSubmitted","errorText","setErrorText","resolved","answer","credentials","handleSubmit","method","headers","JSON","stringify","needsEmail","pending","Boolean","error","mt","reply","pl","borderLeft","tokenProblem","severity","data-review-sign-in","maxWidth","verifiedVia","alignSelf","onChange","_event","prev","placeholder","event","target","multiline","minRows","type","disabled","trim","onClick","schema","$id","pluginId","description","category","ComponentCategory","COMMERCE","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","FieldComponentType","PRODUCT_SELECT","TEXT_FIELD","SELECT","options","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
@@ -424,8 +424,8 @@ export declare function renameProductOptions(product: Pick<HostProduct, 'options
424
424
  /**
425
425
  * Whether a variant has a price a sale can charge (AGL-2916).
426
426
  *
427
- * A product proposed by AI is created with every price left empty for the
428
- * merchant to set, so a stored variant can lack one. `Number(undefined)` is
427
+ * A product may be stored with a price left empty on purpose (an owner who
428
+ * clears it, or a writer asked for none), so a stored variant can lack one. `Number(undefined)` is
429
429
  * no price, and neither is `null`, which `Number` reads as zero: a sale door
430
430
  * that priced either would charge nothing, or fail at the processor.
431
431
  */
@@ -495,6 +495,15 @@ export declare function unpricedProductDraft(proposal: ProposedProduct, takenSlu
495
495
  * worked out again for that price. `null` leaves it unpriced on purpose.
496
496
  */
497
497
  export declare function defaultPricedProductDraft(proposal: ProposedProduct, takenSlugs: ReadonlySet<string>, nowMs: number, priceUsd?: number | null): UnpricedProductDraft | HostProduct;
498
+ /**
499
+ * The price a proposed product is created at (Zach, 2026-10-12): a price an
500
+ * AI proposal suggests for what the product is, when it is a real one — a
501
+ * finite amount above $0, at most `COMMERCE_MAX_PRICE_USD`, rounded to whole
502
+ * cents — else the default price (`COMMERCE_DEFAULT_PRICE_USD`). Never $0,
503
+ * negative or past the ceiling, whatever a proposal carries; the owner
504
+ * changes it like any price.
505
+ */
506
+ export declare function proposedProductPriceUsd(suggested: unknown): number;
498
507
  /** Variant whose option selections match exactly; undefined if none. */
499
508
  export declare function findVariant(product: Pick<HostProduct, 'variants'>, selections: Record<string, string>): ProductVariant | undefined;
500
509
  /** [min, max] price across variants; [0, 0] when there are none. */
@@ -363,8 +363,8 @@ export const COMMERCE_SLUG_MAX_LENGTH = 80;
363
363
  /**
364
364
  * Whether a variant has a price a sale can charge (AGL-2916).
365
365
  *
366
- * A product proposed by AI is created with every price left empty for the
367
- * merchant to set, so a stored variant can lack one. `Number(undefined)` is
366
+ * A product may be stored with a price left empty on purpose (an owner who
367
+ * clears it, or a writer asked for none), so a stored variant can lack one. `Number(undefined)` is
368
368
  * no price, and neither is `null`, which `Number` reads as zero: a sale door
369
369
  * that priced either would charge nothing, or fail at the processor.
370
370
  */ export function variantHasPrice(variant) {
@@ -502,6 +502,18 @@ export const COMMERCE_SLUG_MAX_LENGTH = 80;
502
502
  variants
503
503
  });
504
504
  }
505
+ /**
506
+ * The price a proposed product is created at (Zach, 2026-10-12): a price an
507
+ * AI proposal suggests for what the product is, when it is a real one — a
508
+ * finite amount above $0, at most `COMMERCE_MAX_PRICE_USD`, rounded to whole
509
+ * cents — else the default price (`COMMERCE_DEFAULT_PRICE_USD`). Never $0,
510
+ * negative or past the ceiling, whatever a proposal carries; the owner
511
+ * changes it like any price.
512
+ */ export function proposedProductPriceUsd(suggested) {
513
+ if (typeof suggested !== 'number' || !Number.isFinite(suggested)) return COMMERCE_DEFAULT_PRICE_USD;
514
+ const cents = Math.round(suggested * 100);
515
+ return cents > 0 && cents <= COMMERCE_MAX_PRICE_USD * 100 ? cents / 100 : COMMERCE_DEFAULT_PRICE_USD;
516
+ }
505
517
  /** Variant whose option selections match exactly; undefined if none. */ export function findVariant(product, selections) {
506
518
  var _product_variants;
507
519
  return (_product_variants = product.variants) == null ? void 0 : _product_variants.find((variant)=>{