@aglyn/plugins-commerce 1.0.0-beta.219 → 1.0.0-beta.221

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 (83) hide show
  1. package/README.md +5 -0
  2. package/package.json +14 -14
  3. package/src/lib/components/console/catalog-organization-card.component.js +626 -446
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  6. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  7. package/src/lib/components/console/product-editor-dialog.component.js +1127 -846
  8. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  9. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  10. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  11. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  12. package/src/lib/components/member-signup.js +2 -2
  13. package/src/lib/components/member-signup.js.map +1 -1
  14. package/src/lib/components/newsletter-signup.js +2 -2
  15. package/src/lib/components/newsletter-signup.js.map +1 -1
  16. package/src/lib/constants/operator-alerts.js +5 -3
  17. package/src/lib/constants/operator-alerts.js.map +1 -1
  18. package/src/lib/model/commerce-io.js +1 -1
  19. package/src/lib/model/commerce-io.js.map +1 -1
  20. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  21. package/src/lib/model/commerce-record-lists.js +45 -0
  22. package/src/lib/model/commerce-record-lists.js.map +1 -0
  23. package/src/lib/model/commerce-record-routes.d.ts +18 -0
  24. package/src/lib/model/commerce-record-routes.js +1 -1
  25. package/src/lib/model/commerce-record-routes.js.map +1 -1
  26. package/src/lib/model/commerce-search-sources.js +4 -2
  27. package/src/lib/model/commerce-search-sources.js.map +1 -1
  28. package/src/lib/model/commerce-topics.d.ts +20 -0
  29. package/src/lib/model/commerce-topics.js +33 -0
  30. package/src/lib/model/commerce-topics.js.map +1 -0
  31. package/src/lib/model/index.d.ts +1 -0
  32. package/src/lib/model/index.js +1 -0
  33. package/src/lib/model/index.js.map +1 -1
  34. package/src/lib/model/product-record.d.ts +32 -0
  35. package/src/lib/model/product-record.js +82 -0
  36. package/src/lib/model/product-record.js.map +1 -0
  37. package/src/lib/model/site-member-purchases.d.ts +48 -0
  38. package/src/lib/model/site-member-purchases.js +50 -0
  39. package/src/lib/model/site-member-purchases.js.map +1 -0
  40. package/src/lib/plugin.js +12 -0
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +410 -208
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/cart-checkout.js +1 -1
  45. package/src/lib/server/cart-checkout.js.map +1 -1
  46. package/src/lib/server/gift-cards.js +7 -3
  47. package/src/lib/server/gift-cards.js.map +1 -1
  48. package/src/lib/server/low-stock.js +3 -1
  49. package/src/lib/server/low-stock.js.map +1 -1
  50. package/src/lib/server/member-post.js +2 -2
  51. package/src/lib/server/member-post.js.map +1 -1
  52. package/src/lib/server/membership-admin-password.js +4 -1
  53. package/src/lib/server/membership-admin-password.js.map +1 -1
  54. package/src/lib/server/membership-register.js +13 -2
  55. package/src/lib/server/membership-register.js.map +1 -1
  56. package/src/lib/server/newsletter.js +32 -19
  57. package/src/lib/server/newsletter.js.map +1 -1
  58. package/src/lib/server/pos-order.js +18 -10
  59. package/src/lib/server/pos-order.js.map +1 -1
  60. package/src/lib/server/process-abandoned.js +4 -2
  61. package/src/lib/server/process-abandoned.js.map +1 -1
  62. package/src/lib/server/process-restock.js +7 -5
  63. package/src/lib/server/process-restock.js.map +1 -1
  64. package/src/lib/server/product-record-index.js +5 -37
  65. package/src/lib/server/product-record-index.js.map +1 -1
  66. package/src/lib/server/reconcile-stock.js +1 -1
  67. package/src/lib/server/reconcile-stock.js.map +1 -1
  68. package/src/lib/server/reserve-stock.js +6 -1
  69. package/src/lib/server/reserve-stock.js.map +1 -1
  70. package/src/lib/server/revenue-source.js +3 -3
  71. package/src/lib/server/revenue-source.js.map +1 -1
  72. package/src/lib/server/supplier-outbox.js +16 -1
  73. package/src/lib/server/supplier-outbox.js.map +1 -1
  74. package/src/lib/server/supplier-update.js +19 -9
  75. package/src/lib/server/supplier-update.js.map +1 -1
  76. package/src/lib/starter-templates.d.ts +19 -0
  77. package/src/lib/starter-templates.js +157 -0
  78. package/src/lib/starter-templates.js.map +1 -0
  79. package/src/lib/tenant-emails.js +54 -14
  80. package/src/lib/tenant-emails.js.map +1 -1
  81. package/src/lib/testing/stand-in-record-system.d.ts +11 -2
  82. package/src/lib/testing/stand-in-record-system.js +5 -3
  83. package/src/lib/testing/stand-in-record-system.js.map +1 -1
@@ -0,0 +1,339 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
19
+ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
20
+ import { ceilingedWindow, useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
21
+ import { Chip, Divider, Stack, Typography } from "@mui/material";
22
+ import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
23
+ import { useEffect, useMemo, useState } from "react";
24
+ import { splitOrderReversal } from "../../model/commerce-dispute.js";
25
+ import { computeLifetimePurchaseCents } from "../../model/site-member-purchases.js";
26
+ const usd = (cents)=>`$${(cents / 100).toFixed(2)}`;
27
+ /**
28
+ * How many order documents this reads for one member.
29
+ *
30
+ * A CEILING, not a page size: the lifetime-purchase figure above the list is
31
+ * summed from these rows, so every one it intends to count has to be held. A
32
+ * server page would make that headline number the total of ten orders and
33
+ * print it as a lifetime.
34
+ */ const ORDER_CEILING = 100;
35
+ /** The same, for the subscriptions this member holds. */ const SUBSCRIPTION_CEILING = 25;
36
+ /**
37
+ * The reversal suffix on one order row, split by the door the money left
38
+ * through (AGL-1810). `refundedCents` carries a lost chargeback as well as a
39
+ * refund (AGL-1787 puts both there deliberately), so rendering the whole
40
+ * figure as "refunded" told the merchant they chose a reversal a bank took.
41
+ * `computeLifetimePurchaseCents` keeps netting the total; only the label
42
+ * splits.
43
+ */ const reversalSuffix = (order)=>{
44
+ const { refundedCents, chargedBackCents } = splitOrderReversal(order);
45
+ return (refundedCents ? ` · refunded ${usd(refundedCents)}` : '') + (chargedBackCents ? ` · charged back ${usd(chargedBackCents)}` : '');
46
+ };
47
+ /** Display order number: v1 sequential `#1042`, else a doc-id stub. */ const orderNumber = (order)=>{
48
+ var _order_$id;
49
+ return order.number != null ? `#${order.number}` : `#${String((_order_$id = order.$id) != null ? _order_$id : '').slice(-6).toUpperCase()}`;
50
+ };
51
+ const orderCreatedMs = (order)=>{
52
+ var _order_createdAtMs, _ref;
53
+ var _order_createdAt;
54
+ return Number((_order_createdAtMs = order.createdAtMs) != null ? _order_createdAtMs : ((_ref = (_order_createdAt = order.createdAt) == null ? void 0 : _order_createdAt.seconds) != null ? _ref : 0) * 1000) || 0;
55
+ };
56
+ /**
57
+ * What a site user bought (AGL-546): the lifetime purchase total, the order
58
+ * history (the payment records — Stripe intent id and refunds included) and
59
+ * storefront subscriptions — the `siteMember` zone of the account's drawer.
60
+ *
61
+ * Orders match by email (mirrors membership-account, AGL-294) and sort
62
+ * client-side. The QUERY orders on the document name: `orderBy('createdAt')`
63
+ * would want a composite index and, worse, would drop every order missing the
64
+ * field, while an equality filter plus `orderBy(documentId())` rides the
65
+ * automatic single-field index and can drop nothing — a document's name
66
+ * cannot be absent. That makes the window total rather than a pseudo-random
67
+ * hundred, which is what the newest-first sort below was quietly disguising.
68
+ * `orders` reads are admin/editor-only in rules (AGL-502), so viewers get a
69
+ * note instead of history.
70
+ *
71
+ * One section per element, as a fragment: the zone is `bare`, and the drawer's
72
+ * own column spaces what it holds.
73
+ */ export function SiteMemberPurchases(props) {
74
+ var _member_$id, _member_email, _ref;
75
+ const { hostId, member } = props;
76
+ const firestore = useFirestore();
77
+ const memberId = String((_member_$id = member.$id) != null ? _member_$id : '');
78
+ const email = String((_member_email = member['email']) != null ? _member_email : '');
79
+ const { data: orderDocs, status: ordersStatus } = useFirestoreCollection(()=>email ? query(collection(firestore, 'hosts', hostId, 'orders'), where('customerEmail', '==', email), orderBy(documentId()), // One document past the ceiling, so "this member has more" is a
80
+ // fact. `length === 100` cannot tell a member with exactly a
81
+ // hundred orders from one with a thousand, and the difference is
82
+ // whether the lifetime figure above is a total or a floor.
83
+ limit(ORDER_CEILING + 1)) : null, [
84
+ firestore,
85
+ hostId,
86
+ email
87
+ ], {
88
+ idField: '$id'
89
+ });
90
+ const { rows: readOrders, truncated: ordersTruncated } = ceilingedWindow(orderDocs, ORDER_CEILING);
91
+ const orders = useMemo(()=>[
92
+ ...readOrders
93
+ ].sort((a, b)=>orderCreatedMs(b) - orderCreatedMs(a)), // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ [
95
+ orderDocs
96
+ ]);
97
+ const lifetimeCents = useMemo(()=>computeLifetimePurchaseCents(orders), [
98
+ orders
99
+ ]);
100
+ /**
101
+ * The order query failed AND nothing came back with it (AGL-1066).
102
+ *
103
+ * The copy this gates blames the reader's ROLE, which is the right
104
+ * explanation for the denial this was written for — orders are
105
+ * admin/editor-only in rules. It is the wrong explanation once a stale
106
+ * SESSION can push a listen to `'error'` too, and flatly wrong when
107
+ * `persistentLocalCache` is still serving the rows: telling someone their
108
+ * role is insufficient while their orders sit right there is a support
109
+ * ticket about permissions that was never about permissions.
110
+ */ const ordersUnreadable = ordersStatus === 'error' && orders.length === 0;
111
+ const { data: subscriptionDocs } = useFirestoreCollection(()=>email ? query(collection(firestore, 'hosts', hostId, 'subscriptions'), where('customerEmail', '==', email), // The same decision as the orders above, for the same reason: a
112
+ // subscription written without `createdAt` would be hidden by a
113
+ // field ordering rather than mis-sorted by it.
114
+ orderBy(documentId()), limit(SUBSCRIPTION_CEILING + 1)) : null, [
115
+ firestore,
116
+ hostId,
117
+ email
118
+ ], {
119
+ idField: '$id'
120
+ });
121
+ const { rows: subscriptions, truncated: subscriptionsTruncated } = ceilingedWindow(subscriptionDocs, SUBSCRIPTION_CEILING);
122
+ // Product names for subscriptions, loaded only when any exist.
123
+ const { data: productDocs } = useFirestoreCollection(()=>{
124
+ var _ref;
125
+ return email && ((_ref = subscriptionDocs == null ? void 0 : subscriptionDocs.length) != null ? _ref : 0) > 0 ? query(collection(firestore, 'hosts', hostId, 'products'), // A LOOKUP, not a list — but the same ordering decision, so the
126
+ // hundred it reads is a reachable hundred rather than a sample,
127
+ // and a product past it degrades a subscription's label to its
128
+ // id rather than hiding the subscription.
129
+ orderBy(documentId()), limit(100)) : null;
130
+ }, [
131
+ firestore,
132
+ hostId,
133
+ email,
134
+ ((_ref = subscriptionDocs == null ? void 0 : subscriptionDocs.length) != null ? _ref : 0) > 0
135
+ ], {
136
+ idField: '$id'
137
+ });
138
+ /*==========================================
139
+ * BOTH LISTS PAGE IN MEMORY.
140
+ *
141
+ * Each ceiling is held whole, which is what lets the orders sort
142
+ * newest-first and the lifetime figure sum over all of them. A server page
143
+ * would have made both about ten rows instead of about this member.
144
+ *=========================================*/ const [orderPage, setOrderPage] = useState(0);
145
+ const [orderPageSize, setOrderPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
146
+ const visibleOrders = useMemo(()=>orders.slice(orderPage * orderPageSize, orderPage * orderPageSize + orderPageSize), [
147
+ orders,
148
+ orderPage,
149
+ orderPageSize
150
+ ]);
151
+ const [subscriptionPage, setSubscriptionPage] = useState(0);
152
+ const [subscriptionPageSize, setSubscriptionPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
153
+ const visibleSubscriptions = useMemo(()=>subscriptions.slice(subscriptionPage * subscriptionPageSize, subscriptionPage * subscriptionPageSize + subscriptionPageSize), [
154
+ subscriptions,
155
+ subscriptionPage,
156
+ subscriptionPageSize
157
+ ]);
158
+ /*
159
+ * A different member starts at page one. The drawer is reused for whoever
160
+ * is selected, so without this the next member opens three pages into a
161
+ * history they may not have — which renders as an empty list and reads as
162
+ * "this person never bought anything".
163
+ */ useEffect(()=>{
164
+ setOrderPage(0);
165
+ setSubscriptionPage(0);
166
+ }, [
167
+ memberId
168
+ ]);
169
+ const productNames = useMemo(()=>{
170
+ const map = {};
171
+ for (const product of productDocs != null ? productDocs : []){
172
+ var _product_name;
173
+ map[product.$id] = (_product_name = product.name) != null ? _product_name : product.$id;
174
+ }
175
+ return map;
176
+ }, [
177
+ productDocs
178
+ ]);
179
+ return /*#__PURE__*/ _jsxs(_Fragment, {
180
+ children: [
181
+ /*#__PURE__*/ _jsx(Divider, {
182
+ textAlign: "left",
183
+ children: 'Lifetime purchases'
184
+ }),
185
+ /*#__PURE__*/ _jsx(Typography, {
186
+ variant: "h6",
187
+ children: ordersUnreadable ? '—' : `${ordersTruncated ? 'at least ' : ''}${usd(lifetimeCents)}`
188
+ }),
189
+ /*#__PURE__*/ _jsxs(Typography, {
190
+ variant: "caption",
191
+ color: "text.secondary",
192
+ children: [
193
+ 'Charged order totals minus refunds; pending and canceled ' + 'orders excluded.',
194
+ ordersTruncated ? ` Summed over the ${ORDER_CEILING} orders read here; this ` + 'member has more.' : ''
195
+ ]
196
+ }),
197
+ /*#__PURE__*/ _jsx(Divider, {
198
+ textAlign: "left",
199
+ children: 'Orders'
200
+ }),
201
+ ordersUnreadable ? /*#__PURE__*/ _jsx(Typography, {
202
+ variant: "body2",
203
+ color: "text.secondary",
204
+ children: 'Order history needs the editor or admin role on this site.'
205
+ }) : orders.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
206
+ variant: "body2",
207
+ color: "text.secondary",
208
+ children: 'No orders yet.'
209
+ }) : visibleOrders.map((order)=>{
210
+ var _ref, _ref1, _order_status;
211
+ var _order_totals;
212
+ return /*#__PURE__*/ _jsxs(Stack, {
213
+ spacing: 0,
214
+ children: [
215
+ /*#__PURE__*/ _jsxs(Stack, {
216
+ direction: "row",
217
+ spacing: 1,
218
+ sx: {
219
+ alignItems: 'center'
220
+ },
221
+ children: [
222
+ /*#__PURE__*/ _jsx(Typography, {
223
+ variant: "body2",
224
+ sx: {
225
+ flex: 1
226
+ },
227
+ noWrap: true,
228
+ children: `${orderNumber(order)} · ${usd(Number((_ref = (_ref1 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref1 : order.amountCents) != null ? _ref : 0) || 0)}`
229
+ }),
230
+ /*#__PURE__*/ _jsx(Chip, {
231
+ label: String((_order_status = order.status) != null ? _order_status : 'paid').replace('_', ' '),
232
+ size: "small",
233
+ variant: "outlined"
234
+ })
235
+ ]
236
+ }),
237
+ /*#__PURE__*/ _jsxs(Typography, {
238
+ variant: "caption",
239
+ color: "text.secondary",
240
+ children: [
241
+ orderCreatedMs(order) ? new Date(orderCreatedMs(order)).toLocaleDateString() : '—',
242
+ reversalSuffix(order)
243
+ ]
244
+ }),
245
+ order.paymentIntentId ? /*#__PURE__*/ _jsx(Typography, {
246
+ variant: "caption",
247
+ color: "text.secondary",
248
+ sx: {
249
+ fontFamily: 'monospace'
250
+ },
251
+ noWrap: true,
252
+ children: order.paymentIntentId
253
+ }) : null
254
+ ]
255
+ }, order.$id);
256
+ }),
257
+ ordersUnreadable || orders.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
258
+ page: orderPage,
259
+ pageSize: orderPageSize,
260
+ rowCount: visibleOrders.length,
261
+ // The orders held here — a slice of rows already read, so the
262
+ // total is exact for the window. The lifetime caption above is
263
+ // where the window's own shortfall is stated.
264
+ count: orders.length,
265
+ onPageChange: setOrderPage,
266
+ onPageSizeChange: setOrderPageSize
267
+ }),
268
+ subscriptions.length > 0 ? /*#__PURE__*/ _jsxs(_Fragment, {
269
+ children: [
270
+ /*#__PURE__*/ _jsx(Divider, {
271
+ textAlign: "left",
272
+ children: 'Subscriptions'
273
+ }),
274
+ visibleSubscriptions.map((subscription)=>{
275
+ var _ref, _productNames_subscription_productId, _subscription_invoicesCount, _subscription_status;
276
+ var _subscription_totals;
277
+ return /*#__PURE__*/ _jsxs(Stack, {
278
+ direction: "row",
279
+ spacing: 1,
280
+ sx: {
281
+ alignItems: 'center'
282
+ },
283
+ children: [
284
+ /*#__PURE__*/ _jsxs(Stack, {
285
+ sx: {
286
+ flex: 1,
287
+ minWidth: 0
288
+ },
289
+ children: [
290
+ /*#__PURE__*/ _jsxs(Typography, {
291
+ variant: "body2",
292
+ noWrap: true,
293
+ children: [
294
+ (_ref = (_productNames_subscription_productId = productNames[subscription.productId]) != null ? _productNames_subscription_productId : subscription.productId) != null ? _ref : 'Subscription',
295
+ ((_subscription_totals = subscription.totals) == null ? void 0 : _subscription_totals.totalCents) != null ? ` · ${usd(Number(subscription.totals.totalCents) || 0)}${subscription.interval ? `/${subscription.interval}` : ''}` : ''
296
+ ]
297
+ }),
298
+ /*#__PURE__*/ _jsxs(Typography, {
299
+ variant: "caption",
300
+ color: "text.secondary",
301
+ children: [
302
+ subscription.currentPeriodEndMs ? `Renews ${new Date(Number(subscription.currentPeriodEndMs)).toLocaleDateString()}` : '—',
303
+ Number((_subscription_invoicesCount = subscription.invoicesCount) != null ? _subscription_invoicesCount : 0) > 0 ? ` · ${usd(Number(subscription.paidCents) || 0)} paid over ${Number(subscription.invoicesCount) === 1 ? '1 invoice' : `${Number(subscription.invoicesCount)} invoices`}` : ''
304
+ ]
305
+ })
306
+ ]
307
+ }),
308
+ /*#__PURE__*/ _jsx(Chip, {
309
+ label: String((_subscription_status = subscription.status) != null ? _subscription_status : 'active'),
310
+ size: "small",
311
+ variant: "outlined",
312
+ color: subscription.status === 'active' ? 'success' : 'default'
313
+ })
314
+ ]
315
+ }, subscription.$id);
316
+ }),
317
+ /*#__PURE__*/ _jsx(ListPagination, {
318
+ page: subscriptionPage,
319
+ pageSize: subscriptionPageSize,
320
+ rowCount: visibleSubscriptions.length,
321
+ // This member's subscriptions, in full for the window.
322
+ count: subscriptions.length,
323
+ onPageChange: setSubscriptionPage,
324
+ onPageSizeChange: setSubscriptionPageSize
325
+ }),
326
+ subscriptionsTruncated ? /*#__PURE__*/ _jsx(Typography, {
327
+ variant: "caption",
328
+ color: "text.secondary",
329
+ children: `Showing ${SUBSCRIPTION_CEILING} subscriptions; this ` + 'member has more.'
330
+ }) : null
331
+ ]
332
+ }) : null
333
+ ]
334
+ });
335
+ }
336
+ SiteMemberPurchases.displayName = 'SiteMemberPurchases';
337
+ export default SiteMemberPurchases;
338
+
339
+ //# sourceMappingURL=site-member-purchases.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/site-member-purchases.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 type { ConsoleSiteMemberZoneProps } from '@aglyn/aglyn'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n ceilingedWindow,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Chip, Divider, Stack, Typography } from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport { splitOrderReversal } from '../../model/commerce-dispute'\nimport { computeLifetimePurchaseCents } from '../../model/site-member-purchases'\n\nconst usd = (cents: number) => `$${(cents / 100).toFixed(2)}`\n\n/**\n * How many order documents this reads for one member.\n *\n * A CEILING, not a page size: the lifetime-purchase figure above the list is\n * summed from these rows, so every one it intends to count has to be held. A\n * server page would make that headline number the total of ten orders and\n * print it as a lifetime.\n */\nconst ORDER_CEILING = 100\n/** The same, for the subscriptions this member holds. */\nconst SUBSCRIPTION_CEILING = 25\n\n/**\n * The reversal suffix on one order row, split by the door the money left\n * through (AGL-1810). `refundedCents` carries a lost chargeback as well as a\n * refund (AGL-1787 puts both there deliberately), so rendering the whole\n * figure as \"refunded\" told the merchant they chose a reversal a bank took.\n * `computeLifetimePurchaseCents` keeps netting the total; only the label\n * splits.\n */\nconst reversalSuffix = (order: any) => {\n const { refundedCents, chargedBackCents } = splitOrderReversal(order)\n return (\n (refundedCents ? ` · refunded ${usd(refundedCents)}` : '') +\n (chargedBackCents ? ` · charged back ${usd(chargedBackCents)}` : '')\n )\n}\n\n/** Display order number: v1 sequential `#1042`, else a doc-id stub. */\nconst orderNumber = (order: any) =>\n order.number != null\n ? `#${order.number}`\n : `#${String(order.$id ?? '').slice(-6).toUpperCase()}`\n\nconst orderCreatedMs = (order: any) =>\n Number(order.createdAtMs ?? (order.createdAt?.seconds ?? 0) * 1000) || 0\n\n/**\n * What a site user bought (AGL-546): the lifetime purchase total, the order\n * history (the payment records — Stripe intent id and refunds included) and\n * storefront subscriptions — the `siteMember` zone of the account's drawer.\n *\n * Orders match by email (mirrors membership-account, AGL-294) and sort\n * client-side. The QUERY orders on the document name: `orderBy('createdAt')`\n * would want a composite index and, worse, would drop every order missing the\n * field, while an equality filter plus `orderBy(documentId())` rides the\n * automatic single-field index and can drop nothing — a document's name\n * cannot be absent. That makes the window total rather than a pseudo-random\n * hundred, which is what the newest-first sort below was quietly disguising.\n * `orders` reads are admin/editor-only in rules (AGL-502), so viewers get a\n * note instead of history.\n *\n * One section per element, as a fragment: the zone is `bare`, and the drawer's\n * own column spaces what it holds.\n */\nexport function SiteMemberPurchases(props: ConsoleSiteMemberZoneProps) {\n const { hostId, member } = props\n const firestore = useFirestore()\n const memberId = String(member.$id ?? '')\n const email = String(member['email'] ?? '')\n\n const { data: orderDocs, status: ordersStatus } = useFirestoreCollection<any>(\n () =>\n email\n ? query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('customerEmail', '==', email),\n orderBy(documentId()),\n // One document past the ceiling, so \"this member has more\" is a\n // fact. `length === 100` cannot tell a member with exactly a\n // hundred orders from one with a thousand, and the difference is\n // whether the lifetime figure above is a total or a floor.\n limit(ORDER_CEILING + 1),\n )\n : null,\n [firestore, hostId, email],\n { idField: '$id' },\n )\n const { rows: readOrders, truncated: ordersTruncated } = ceilingedWindow<any>(\n orderDocs,\n ORDER_CEILING,\n )\n const orders = useMemo(\n () =>\n [...readOrders].sort((a, b) => orderCreatedMs(b) - orderCreatedMs(a)),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [orderDocs],\n )\n const lifetimeCents = useMemo(\n () => computeLifetimePurchaseCents(orders),\n [orders],\n )\n /**\n * The order query failed AND nothing came back with it (AGL-1066).\n *\n * The copy this gates blames the reader's ROLE, which is the right\n * explanation for the denial this was written for — orders are\n * admin/editor-only in rules. It is the wrong explanation once a stale\n * SESSION can push a listen to `'error'` too, and flatly wrong when\n * `persistentLocalCache` is still serving the rows: telling someone their\n * role is insufficient while their orders sit right there is a support\n * ticket about permissions that was never about permissions.\n */\n const ordersUnreadable = ordersStatus === 'error' && orders.length === 0\n\n const { data: subscriptionDocs } = useFirestoreCollection<any>(\n () =>\n email\n ? query(\n collection(firestore, 'hosts', hostId, 'subscriptions'),\n where('customerEmail', '==', email),\n // The same decision as the orders above, for the same reason: a\n // subscription written without `createdAt` would be hidden by a\n // field ordering rather than mis-sorted by it.\n orderBy(documentId()),\n limit(SUBSCRIPTION_CEILING + 1),\n )\n : null,\n [firestore, hostId, email],\n { idField: '$id' },\n )\n const { rows: subscriptions, truncated: subscriptionsTruncated } =\n ceilingedWindow<any>(subscriptionDocs, SUBSCRIPTION_CEILING)\n // Product names for subscriptions, loaded only when any exist.\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n email && (subscriptionDocs?.length ?? 0) > 0\n ? query(\n collection(firestore, 'hosts', hostId, 'products'),\n // A LOOKUP, not a list — but the same ordering decision, so the\n // hundred it reads is a reachable hundred rather than a sample,\n // and a product past it degrades a subscription's label to its\n // id rather than hiding the subscription.\n orderBy(documentId()),\n limit(100),\n )\n : null,\n [firestore, hostId, email, (subscriptionDocs?.length ?? 0) > 0],\n { idField: '$id' },\n )\n /*==========================================\n * BOTH LISTS PAGE IN MEMORY.\n *\n * Each ceiling is held whole, which is what lets the orders sort\n * newest-first and the lifetime figure sum over all of them. A server page\n * would have made both about ten rows instead of about this member.\n *=========================================*/\n const [orderPage, setOrderPage] = useState(0)\n const [orderPageSize, setOrderPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const visibleOrders = useMemo(\n () =>\n orders.slice(\n orderPage * orderPageSize,\n orderPage * orderPageSize + orderPageSize,\n ),\n [orders, orderPage, orderPageSize],\n )\n const [subscriptionPage, setSubscriptionPage] = useState(0)\n const [subscriptionPageSize, setSubscriptionPageSize] = useState(\n TABLE_PAGE_SIZE_DEFAULT,\n )\n const visibleSubscriptions = useMemo(\n () =>\n subscriptions.slice(\n subscriptionPage * subscriptionPageSize,\n subscriptionPage * subscriptionPageSize + subscriptionPageSize,\n ),\n [subscriptions, subscriptionPage, subscriptionPageSize],\n )\n /*\n * A different member starts at page one. The drawer is reused for whoever\n * is selected, so without this the next member opens three pages into a\n * history they may not have — which renders as an empty list and reads as\n * \"this person never bought anything\".\n */\n useEffect(() => {\n setOrderPage(0)\n setSubscriptionPage(0)\n }, [memberId])\n\n const productNames = useMemo(() => {\n const map: Record<string, string> = {}\n for (const product of productDocs ?? []) {\n map[product.$id] = product.name ?? product.$id\n }\n return map\n }, [productDocs])\n\n return (\n <>\n <Divider textAlign=\"left\">{'Lifetime purchases'}</Divider>\n <Typography variant=\"h6\">\n {/*\n \"at least\", or nothing at all. This figure is summed over the order\n WINDOW, so once the probe finds an order past the ceiling it is a\n lower bound — and a lower bound printed as a lifetime is the number\n a support agent quotes back to the customer.\n */}\n {ordersUnreadable\n ? '—'\n : `${ordersTruncated ? 'at least ' : ''}${usd(lifetimeCents)}`}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Charged order totals minus refunds; pending and canceled ' +\n 'orders excluded.'}\n {ordersTruncated\n ? ` Summed over the ${ORDER_CEILING} orders read here; this ` +\n 'member has more.'\n : ''}\n </Typography>\n\n <Divider textAlign=\"left\">{'Orders'}</Divider>\n {ordersUnreadable ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Order history needs the editor or admin role on this site.'}\n </Typography>\n ) : orders.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No orders yet.'}\n </Typography>\n ) : (\n visibleOrders.map((order: any) => (\n <Stack key={order.$id} spacing={0}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {`${orderNumber(order)} · ${usd(\n Number(order.totals?.totalCents ?? order.amountCents ?? 0) ||\n 0,\n )}`}\n </Typography>\n <Chip\n label={String(order.status ?? 'paid').replace('_', ' ')}\n size=\"small\"\n variant=\"outlined\"\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {orderCreatedMs(order)\n ? new Date(orderCreatedMs(order)).toLocaleDateString()\n : '—'}\n {reversalSuffix(order)}\n </Typography>\n {order.paymentIntentId ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ fontFamily: 'monospace' }}\n noWrap\n >\n {order.paymentIntentId}\n </Typography>\n ) : null}\n </Stack>\n ))\n )}\n {ordersUnreadable || orders.length === 0 ? null : (\n <ListPagination\n page={orderPage}\n pageSize={orderPageSize}\n rowCount={visibleOrders.length}\n // The orders held here — a slice of rows already read, so the\n // total is exact for the window. The lifetime caption above is\n // where the window's own shortfall is stated.\n count={orders.length}\n onPageChange={setOrderPage}\n onPageSizeChange={setOrderPageSize}\n />\n )}\n\n {subscriptions.length > 0 ? (\n <>\n <Divider textAlign=\"left\">{'Subscriptions'}</Divider>\n {visibleSubscriptions.map((subscription: any) => (\n <Stack\n key={subscription.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {/*\n * The amount (AGL-1732). This row named the product and the\n * renewal date and stopped, and no other console surface\n * carried the figure either — orders, analytics and the CSV\n * all read `orders`, which a subscription sale does not\n * create. Subscriptions written before that fix have no\n * `totals`, so the amount is omitted rather than shown as\n * $0.00.\n */}\n {productNames[subscription.productId] ??\n subscription.productId ??\n 'Subscription'}\n {subscription.totals?.totalCents != null\n ? ` · ${usd(Number(subscription.totals.totalCents) || 0)}${\n subscription.interval ? `/${subscription.interval}` : ''\n }`\n : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {subscription.currentPeriodEndMs\n ? `Renews ${new Date(\n Number(subscription.currentPeriodEndMs),\n ).toLocaleDateString()}`\n : '—'}\n {/*\n * What this subscriber has actually paid, across every cycle\n * (AGL-1743). Omitted, not shown as 0, for subscriptions\n * whose cycles all predate `invoice.payment_succeeded`\n * being handled: those invoices are recoverable only from\n * Stripe.\n */}\n {Number(subscription.invoicesCount ?? 0) > 0\n ? ` · ${usd(Number(subscription.paidCents) || 0)} paid over ${\n Number(subscription.invoicesCount) === 1\n ? '1 invoice'\n : `${Number(subscription.invoicesCount)} invoices`\n }`\n : ''}\n </Typography>\n </Stack>\n <Chip\n label={String(subscription.status ?? 'active')}\n size=\"small\"\n variant=\"outlined\"\n color={subscription.status === 'active' ? 'success' : 'default'}\n />\n </Stack>\n ))}\n <ListPagination\n page={subscriptionPage}\n pageSize={subscriptionPageSize}\n rowCount={visibleSubscriptions.length}\n // This member's subscriptions, in full for the window.\n count={subscriptions.length}\n onPageChange={setSubscriptionPage}\n onPageSizeChange={setSubscriptionPageSize}\n />\n {subscriptionsTruncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing ${SUBSCRIPTION_CEILING} subscriptions; this ` +\n 'member has more.'}\n </Typography>\n ) : null}\n </>\n ) : null}\n </>\n )\n}\nSiteMemberPurchases.displayName = 'SiteMemberPurchases'\n\nexport default SiteMemberPurchases\n"],"names":["ListPagination","TABLE_PAGE_SIZE_DEFAULT","ceilingedWindow","useFirestore","useFirestoreCollection","Chip","Divider","Stack","Typography","collection","documentId","limit","orderBy","query","where","useEffect","useMemo","useState","splitOrderReversal","computeLifetimePurchaseCents","usd","cents","toFixed","ORDER_CEILING","SUBSCRIPTION_CEILING","reversalSuffix","order","refundedCents","chargedBackCents","orderNumber","number","String","$id","slice","toUpperCase","orderCreatedMs","Number","createdAtMs","createdAt","seconds","SiteMemberPurchases","props","member","hostId","firestore","memberId","email","data","orderDocs","status","ordersStatus","idField","rows","readOrders","truncated","ordersTruncated","orders","sort","a","b","lifetimeCents","ordersUnreadable","length","subscriptionDocs","subscriptions","subscriptionsTruncated","productDocs","orderPage","setOrderPage","orderPageSize","setOrderPageSize","visibleOrders","subscriptionPage","setSubscriptionPage","subscriptionPageSize","setSubscriptionPageSize","visibleSubscriptions","productNames","map","product","name","textAlign","variant","color","spacing","direction","sx","alignItems","flex","noWrap","totals","totalCents","amountCents","label","replace","size","Date","toLocaleDateString","paymentIntentId","fontFamily","page","pageSize","rowCount","count","onPageChange","onPageSizeChange","subscription","minWidth","productId","interval","currentPeriodEndMs","invoicesCount","paidCents","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,cAAc,QAAQ,4DAA2D;AAC1F,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,SACEC,eAAe,EACfC,YAAY,EACZC,sBAAsB,QACjB,iCAAgC;AACvC,SAASC,IAAI,EAAEC,OAAO,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAChE,SACEC,UAAU,EACVC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAC3B,SAASC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,kBAAkB,QAAQ,kCAA8B;AACjE,SAASC,4BAA4B,QAAQ,uCAAmC;AAEhF,MAAMC,MAAM,CAACC,QAAkB,CAAC,CAAC,EAAE,AAACA,CAAAA,QAAQ,GAAE,EAAGC,OAAO,CAAC,IAAI;AAE7D;;;;;;;CAOC,GACD,MAAMC,gBAAgB;AACtB,uDAAuD,GACvD,MAAMC,uBAAuB;AAE7B;;;;;;;CAOC,GACD,MAAMC,iBAAiB,CAACC;IACtB,MAAM,EAAEC,aAAa,EAAEC,gBAAgB,EAAE,GAAGV,mBAAmBQ;IAC/D,OACE,AAACC,CAAAA,gBAAgB,CAAC,YAAY,EAAEP,IAAIO,gBAAgB,GAAG,EAAC,IACvDC,CAAAA,mBAAmB,CAAC,gBAAgB,EAAER,IAAIQ,mBAAmB,GAAG,EAAC;AAEtE;AAEA,qEAAqE,GACrE,MAAMC,cAAc,CAACH;QAGJA;WAFfA,MAAMI,MAAM,IAAI,OACZ,CAAC,CAAC,EAAEJ,MAAMI,MAAM,EAAE,GAClB,CAAC,CAAC,EAAEC,QAAOL,aAAAA,MAAMM,GAAG,YAATN,aAAa,IAAIO,KAAK,CAAC,CAAC,GAAGC,WAAW,IAAI;;AAE3D,MAAMC,iBAAiB,CAACT;QACfA;QAAsBA;WAA7BU,QAAOV,qBAAAA,MAAMW,WAAW,YAAjBX,qBAAqB,UAACA,mBAAAA,MAAMY,SAAS,qBAAfZ,iBAAiBa,OAAO,mBAAI,KAAK,SAAS;;AAEzE;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,oBAAoBC,KAAiC;QAG3CC,aACHA;IAHrB,MAAM,EAAEC,MAAM,EAAED,MAAM,EAAE,GAAGD;IAC3B,MAAMG,YAAYzC;IAClB,MAAM0C,WAAWd,QAAOW,cAAAA,OAAOV,GAAG,YAAVU,cAAc;IACtC,MAAMI,QAAQf,QAAOW,gBAAAA,MAAM,CAAC,QAAQ,YAAfA,gBAAmB;IAExC,MAAM,EAAEK,MAAMC,SAAS,EAAEC,QAAQC,YAAY,EAAE,GAAG9C,uBAChD,IACE0C,QACIjC,MACEJ,WAAWmC,WAAW,SAASD,QAAQ,WACvC7B,MAAM,iBAAiB,MAAMgC,QAC7BlC,QAAQF,eACR,gEAAgE;QAChE,6DAA6D;QAC7D,iEAAiE;QACjE,2DAA2D;QAC3DC,MAAMY,gBAAgB,MAExB,MACN;QAACqB;QAAWD;QAAQG;KAAM,EAC1B;QAAEK,SAAS;IAAM;IAEnB,MAAM,EAAEC,MAAMC,UAAU,EAAEC,WAAWC,eAAe,EAAE,GAAGrD,gBACvD8C,WACAzB;IAEF,MAAMiC,SAASxC,QACb,IACE;eAAIqC;SAAW,CAACI,IAAI,CAAC,CAACC,GAAGC,IAAMxB,eAAewB,KAAKxB,eAAeuB,KACpE,uDAAuD;IACvD;QAACV;KAAU;IAEb,MAAMY,gBAAgB5C,QACpB,IAAMG,6BAA6BqC,SACnC;QAACA;KAAO;IAEV;;;;;;;;;;GAUC,GACD,MAAMK,mBAAmBX,iBAAiB,WAAWM,OAAOM,MAAM,KAAK;IAEvE,MAAM,EAAEf,MAAMgB,gBAAgB,EAAE,GAAG3D,uBACjC,IACE0C,QACIjC,MACEJ,WAAWmC,WAAW,SAASD,QAAQ,kBACvC7B,MAAM,iBAAiB,MAAMgC,QAC7B,gEAAgE;QAChE,gEAAgE;QAChE,+CAA+C;QAC/ClC,QAAQF,eACRC,MAAMa,uBAAuB,MAE/B,MACN;QAACoB;QAAWD;QAAQG;KAAM,EAC1B;QAAEK,SAAS;IAAM;IAEnB,MAAM,EAAEC,MAAMY,aAAa,EAAEV,WAAWW,sBAAsB,EAAE,GAC9D/D,gBAAqB6D,kBAAkBvC;IACzC,+DAA+D;IAC/D,MAAM,EAAEuB,MAAMmB,WAAW,EAAE,GAAG9D,uBAC5B;;eACE0C,SAAS,SAACiB,oCAAAA,iBAAkBD,MAAM,mBAAI,KAAK,IACvCjD,MACEJ,WAAWmC,WAAW,SAASD,QAAQ,aACvC,gEAAgE;QAChE,gEAAgE;QAChE,+DAA+D;QAC/D,0CAA0C;QAC1C/B,QAAQF,eACRC,MAAM,QAER;OACN;QAACiC;QAAWD;QAAQG;QAAO,SAACiB,oCAAAA,iBAAkBD,MAAM,mBAAI,KAAK;KAAE,EAC/D;QAAEX,SAAS;IAAM;IAEnB;;;;;;6CAM2C,GAC3C,MAAM,CAACgB,WAAWC,aAAa,GAAGnD,SAAS;IAC3C,MAAM,CAACoD,eAAeC,iBAAiB,GAAGrD,SAAShB;IACnD,MAAMsE,gBAAgBvD,QACpB,IACEwC,OAAOvB,KAAK,CACVkC,YAAYE,eACZF,YAAYE,gBAAgBA,gBAEhC;QAACb;QAAQW;QAAWE;KAAc;IAEpC,MAAM,CAACG,kBAAkBC,oBAAoB,GAAGxD,SAAS;IACzD,MAAM,CAACyD,sBAAsBC,wBAAwB,GAAG1D,SACtDhB;IAEF,MAAM2E,uBAAuB5D,QAC3B,IACEgD,cAAc/B,KAAK,CACjBuC,mBAAmBE,sBACnBF,mBAAmBE,uBAAuBA,uBAE9C;QAACV;QAAeQ;QAAkBE;KAAqB;IAEzD;;;;;GAKC,GACD3D,UAAU;QACRqD,aAAa;QACbK,oBAAoB;IACtB,GAAG;QAAC5B;KAAS;IAEb,MAAMgC,eAAe7D,QAAQ;QAC3B,MAAM8D,MAA8B,CAAC;QACrC,KAAK,MAAMC,WAAWb,sBAAAA,cAAe,EAAE,CAAE;gBACpBa;YAAnBD,GAAG,CAACC,QAAQ/C,GAAG,CAAC,IAAG+C,gBAAAA,QAAQC,IAAI,YAAZD,gBAAgBA,QAAQ/C,GAAG;QAChD;QACA,OAAO8C;IACT,GAAG;QAACZ;KAAY;IAEhB,qBACE;;0BACE,KAAC5D;gBAAQ2E,WAAU;0BAAQ;;0BAC3B,KAACzE;gBAAW0E,SAAQ;0BAOjBrB,mBACG,MACA,GAAGN,kBAAkB,cAAc,KAAKnC,IAAIwC,gBAAgB;;0BAElE,MAACpD;gBAAW0E,SAAQ;gBAAUC,OAAM;;oBACjC,8DACC;oBACD5B,kBACG,CAAC,iBAAiB,EAAEhC,cAAc,wBAAwB,CAAC,GAC3D,qBACA;;;0BAGN,KAACjB;gBAAQ2E,WAAU;0BAAQ;;YAC1BpB,iCACC,KAACrD;gBAAW0E,SAAQ;gBAAQC,OAAM;0BAC/B;iBAED3B,OAAOM,MAAM,KAAK,kBACpB,KAACtD;gBAAW0E,SAAQ;gBAAQC,OAAM;0BAC/B;iBAGHZ,cAAcO,GAAG,CAAC,CAACpD;oBAKFA,aAKKA;oBALLA;qCAJf,MAACnB;oBAAsB6E,SAAS;;sCAC9B,MAAC7E;4BAAM8E,WAAU;4BAAMD,SAAS;4BAAGE,IAAI;gCAAEC,YAAY;4BAAS;;8CAC5D,KAAC/E;oCAAW0E,SAAQ;oCAAQI,IAAI;wCAAEE,MAAM;oCAAE;oCAAGC,MAAM;8CAChD,GAAG5D,YAAYH,OAAO,GAAG,EAAEN,IAC1BgB,QAAOV,iBAAAA,gBAAAA,MAAMgE,MAAM,qBAAZhE,cAAciE,UAAU,oBAAIjE,MAAMkE,WAAW,YAA7ClE,OAAiD,MACtD,IACD;;8CAEL,KAACrB;oCACCwF,OAAO9D,QAAOL,gBAAAA,MAAMuB,MAAM,YAAZvB,gBAAgB,QAAQoE,OAAO,CAAC,KAAK;oCACnDC,MAAK;oCACLb,SAAQ;;;;sCAGZ,MAAC1E;4BAAW0E,SAAQ;4BAAUC,OAAM;;gCACjChD,eAAeT,SACZ,IAAIsE,KAAK7D,eAAeT,QAAQuE,kBAAkB,KAClD;gCACHxE,eAAeC;;;wBAEjBA,MAAMwE,eAAe,iBACpB,KAAC1F;4BACC0E,SAAQ;4BACRC,OAAM;4BACNG,IAAI;gCAAEa,YAAY;4BAAY;4BAC9BV,MAAM;sCAEL/D,MAAMwE,eAAe;6BAEtB;;mBA7BMxE,MAAMM,GAAG;;YAiCxB6B,oBAAoBL,OAAOM,MAAM,KAAK,IAAI,qBACzC,KAAC9D;gBACCoG,MAAMjC;gBACNkC,UAAUhC;gBACViC,UAAU/B,cAAcT,MAAM;gBAC9B,8DAA8D;gBAC9D,+DAA+D;gBAC/D,8CAA8C;gBAC9CyC,OAAO/C,OAAOM,MAAM;gBACpB0C,cAAcpC;gBACdqC,kBAAkBnC;;YAIrBN,cAAcF,MAAM,GAAG,kBACtB;;kCACE,KAACxD;wBAAQ2E,WAAU;kCAAQ;;oBAC1BL,qBAAqBE,GAAG,CAAC,CAAC4B;4BAkBlB7B,MAAAA,sCAsBO6B,6BAUIA;4BA7BXA;6CApBP,MAACnG;4BAEC8E,WAAU;4BACVD,SAAS;4BACTE,IAAI;gCAAEC,YAAY;4BAAS;;8CAE3B,MAAChF;oCAAM+E,IAAI;wCAAEE,MAAM;wCAAGmB,UAAU;oCAAE;;sDAChC,MAACnG;4CAAW0E,SAAQ;4CAAQO,MAAM;;iDAU/BZ,QAAAA,uCAAAA,YAAY,CAAC6B,aAAaE,SAAS,CAAC,YAApC/B,uCACC6B,aAAaE,SAAS,YADvB/B,OAEC;gDACD6B,EAAAA,uBAAAA,aAAahB,MAAM,qBAAnBgB,qBAAqBf,UAAU,KAAI,OAChC,CAAC,GAAG,EAAEvE,IAAIgB,OAAOsE,aAAahB,MAAM,CAACC,UAAU,KAAK,KAClDe,aAAaG,QAAQ,GAAG,CAAC,CAAC,EAAEH,aAAaG,QAAQ,EAAE,GAAG,IACtD,GACF;;;sDAEN,MAACrG;4CAAW0E,SAAQ;4CAAUC,OAAM;;gDACjCuB,aAAaI,kBAAkB,GAC5B,CAAC,OAAO,EAAE,IAAId,KACZ5D,OAAOsE,aAAaI,kBAAkB,GACtCb,kBAAkB,IAAI,GACxB;gDAQH7D,QAAOsE,8BAAAA,aAAaK,aAAa,YAA1BL,8BAA8B,KAAK,IACvC,CAAC,GAAG,EAAEtF,IAAIgB,OAAOsE,aAAaM,SAAS,KAAK,GAAG,WAAW,EACxD5E,OAAOsE,aAAaK,aAAa,MAAM,IACnC,cACA,GAAG3E,OAAOsE,aAAaK,aAAa,EAAE,SAAS,CAAC,EACpD,GACF;;;;;8CAGR,KAAC1G;oCACCwF,OAAO9D,QAAO2E,uBAAAA,aAAazD,MAAM,YAAnByD,uBAAuB;oCACrCX,MAAK;oCACLb,SAAQ;oCACRC,OAAOuB,aAAazD,MAAM,KAAK,WAAW,YAAY;;;2BAnDnDyD,aAAa1E,GAAG;;kCAuDzB,KAAChC;wBACCoG,MAAM5B;wBACN6B,UAAU3B;wBACV4B,UAAU1B,qBAAqBd,MAAM;wBACrC,uDAAuD;wBACvDyC,OAAOvC,cAAcF,MAAM;wBAC3B0C,cAAc/B;wBACdgC,kBAAkB9B;;oBAEnBV,uCACC,KAACzD;wBAAW0E,SAAQ;wBAAUC,OAAM;kCACjC,CAAC,QAAQ,EAAE3D,qBAAqB,qBAAqB,CAAC,GACrD;yBAEF;;iBAEJ;;;AAGV;AACAgB,oBAAoByE,WAAW,GAAG;AAElC,eAAezE,oBAAmB"}
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  * limitations under the License.
19
19
  */ import * as Aglyn from "@aglyn/aglyn";
20
20
  import { trackEventBeforeNavigation } from "@aglyn/aglyn/app-utils/analytics-events";
21
- import { campaignTouchField } from "@aglyn/aglyn/app-utils/campaign-touch";
21
+ import { utmTouchField } from "@aglyn/aglyn/app-utils/utm-touch";
22
22
  import { mdiAccountPlusOutline } from "@aglyn/shared-data-mdi";
23
23
  import Alert from "@mui/material/Alert";
24
24
  import Box from "@mui/material/Box";
@@ -76,7 +76,7 @@ export const ID = 'member-signup';
76
76
  displayName
77
77
  } : {}, marketingConsent ? {
78
78
  marketingConsent: true
79
- } : {}, campaignTouchField()))
79
+ } : {}, utmTouchField()))
80
80
  });
81
81
  if (!response.ok) {
82
82
  var _ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/member-signup.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 { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { campaignTouchField } from '@aglyn/aglyn/app-utils/campaign-touch'\nimport { mdiAccountPlusOutline } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport Link from '@mui/material/Link'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useCallback, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n continuePathFromLocation,\n memberNavigation,\n} from '../utils/member-continue'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'member-signup'\n\nexport interface MemberSignupProps {\n heading?: string\n signinPath?: string\n continueFallback?: string\n}\n\n/**\n * Standalone sign-up block (AGL-553), extracted from the customer-account\n * create-account tab. Registers through the AGL-109 membership API (with\n * the same belt-and-braces follow-up login the account block does), then\n * sends the new member to the validated `continue` query param (relative\n * paths only) or the configured fallback.\n */\nconst MemberSignup = forwardRef<HTMLDivElement, MemberSignupProps>(\n (props, ref) => {\n const { heading, signinPath, continueFallback, ...rest } = 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 [displayName, setDisplayName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [error, setError] = useState('')\n const [busy, setBusy] = useState(false)\n\n const handleSignup = useCallback(async () => {\n if (!hostId || busy) return\n setBusy(true)\n setError('')\n try {\n const response = await siteFetch('/api/membership/register', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n email,\n password,\n ...(displayName ? { displayName } : {}),\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // The campaign this visitor came from, when they came from one.\n // A sign-up is the identify moment that also writes a lead, so\n // the touch has to arrive with the request that creates it.\n ...campaignTouchField(),\n }),\n })\n if (!response.ok) {\n const payload = await response.json().catch(() => ({}))\n setError(String(payload?.error ?? 'Sign-up failed'))\n return\n }\n // Register does not set the cookie on every deployment; sign in\n // right after to be safe (same as the customer-account block).\n await siteFetch('/api/membership/login', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, email, password }),\n }).catch(() => undefined)\n /*\n * The host's own GA4 property, not Aglyn's (AGL-1591). A storefront\n * that switched its members to accounts had no way to see it happen:\n * `sign_up` is a GA4 recommended event and every other funnel step on\n * this site already reports one, so the account door was the single\n * hole in a funnel the operator can otherwise follow end to end.\n *\n * AWAITED, because the line below tears the document down. Delivery on\n * this surface is a synchronous `window.gtag` call today, so the await\n * costs a microtask — it is written this way because the property that\n * makes a bare call safe is invisible from here, and the day a host\n * registers an async transport the event would start disappearing on\n * exactly the branch that navigates.\n */\n await trackEventBeforeNavigation('sign_up', { method: 'password' })\n memberNavigation.assign(\n continuePathFromLocation(continueFallback || '/'),\n )\n } finally {\n setBusy(false)\n }\n }, [\n hostId,\n busy,\n displayName,\n email,\n password,\n marketingConsent,\n continueFallback,\n siteFetch,\n ])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\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 },\n ...nodeSx,\n ]}\n >\n {'Member sign-up — the create-account form renders here'}\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest} sx={[{ maxWidth: 420 }, ...nodeSx]}>\n {heading ? (\n <Typography variant=\"h5\" gutterBottom>\n {heading}\n </Typography>\n ) : null}\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\n <TextField\n label=\"Name\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n size=\"small\"\n />\n <TextField\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n />\n <TextField\n label=\"Password\"\n type=\"password\"\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n size=\"small\"\n helperText=\"At least 8 characters\"\n />\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={marketingConsent}\n onChange={(event) =>\n setMarketingConsent(event.target.checked)\n }\n />\n }\n label=\"Email me about offers and updates\"\n />\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !email || password.length < 8}\n onClick={handleSignup}\n >\n {busy ? 'Working…' : 'Create account'}\n </Button>\n <Typography variant=\"body2\">\n <Link href={signinPath || '/signin'} underline=\"hover\">\n {'Already a member? Sign in'}\n </Link>\n </Typography>\n </Box>\n </Box>\n )\n },\n)\nMemberSignup.displayName = 'AglynMemberSignup'\n\nexport const schema: Aglyn.ComponentSchema<MemberSignupProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Member sign-up',\n description: 'A create-account form for site membership.',\n category: Aglyn.ComponentCategory.MEMBERS,\n icon: { path: mdiAccountPlusOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'heading',\n label: 'Heading',\n description:\n 'The title above the sign-up fields. Say what joining gets them; ' +\n 'blank renders no heading at all.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'signinPath',\n label: 'Sign-in path',\n description: 'Where \"Already a member?\" points (default /signin).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'continueFallback',\n label: 'After sign-up',\n description:\n 'Where a new member goes once the account EXISTS, when nothing sent ' +\n 'them here. A `?continue=` on the URL always wins. Point it at a ' +\n 'welcome or onboarding page rather than the home page if you have ' +\n 'one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Member sign-up',\n pluginId: BUNDLE_ID,\n description: 'Create-account form for site membership',\n category: Aglyn.ComponentCategory.MEMBERS,\n icon: { path: mdiAccountPlusOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { heading: 'Create your account' },\n },\n },\n]\n\nexport default MemberSignup\n"],"names":["Aglyn","trackEventBeforeNavigation","campaignTouchField","mdiAccountPlusOutline","Alert","Box","Button","Checkbox","FormControlLabel","Link","TextField","Typography","forwardRef","useCallback","useState","BUNDLE_ID","continuePathFromLocation","memberNavigation","generatePresetId","ID","MemberSignup","props","ref","heading","signinPath","continueFallback","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","displayName","setDisplayName","email","setEmail","password","setPassword","marketingConsent","setMarketingConsent","error","setError","busy","setBusy","handleSignup","response","method","headers","body","JSON","stringify","ok","payload","json","catch","String","undefined","assign","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","maxWidth","variant","gutterBottom","display","flexDirection","gap","label","value","onChange","event","target","size","type","helperText","control","checked","severity","disabled","length","onClick","href","underline","schema","$id","pluginId","description","category","ComponentCategory","MEMBERS","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","component","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/member-signup.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 { trackEventBeforeNavigation } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport { mdiAccountPlusOutline } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport Link from '@mui/material/Link'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useCallback, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n continuePathFromLocation,\n memberNavigation,\n} from '../utils/member-continue'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'member-signup'\n\nexport interface MemberSignupProps {\n heading?: string\n signinPath?: string\n continueFallback?: string\n}\n\n/**\n * Standalone sign-up block (AGL-553), extracted from the customer-account\n * create-account tab. Registers through the AGL-109 membership API (with\n * the same belt-and-braces follow-up login the account block does), then\n * sends the new member to the validated `continue` query param (relative\n * paths only) or the configured fallback.\n */\nconst MemberSignup = forwardRef<HTMLDivElement, MemberSignupProps>(\n (props, ref) => {\n const { heading, signinPath, continueFallback, ...rest } = 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 [displayName, setDisplayName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [error, setError] = useState('')\n const [busy, setBusy] = useState(false)\n\n const handleSignup = useCallback(async () => {\n if (!hostId || busy) return\n setBusy(true)\n setError('')\n try {\n const response = await siteFetch('/api/membership/register', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n email,\n password,\n ...(displayName ? { displayName } : {}),\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // The campaign this visitor came from, when they came from one.\n // A sign-up is the identify moment that also writes a lead, so\n // the touch has to arrive with the request that creates it.\n ...utmTouchField(),\n }),\n })\n if (!response.ok) {\n const payload = await response.json().catch(() => ({}))\n setError(String(payload?.error ?? 'Sign-up failed'))\n return\n }\n // Register does not set the cookie on every deployment; sign in\n // right after to be safe (same as the customer-account block).\n await siteFetch('/api/membership/login', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, email, password }),\n }).catch(() => undefined)\n /*\n * The host's own GA4 property, not Aglyn's (AGL-1591). A storefront\n * that switched its members to accounts had no way to see it happen:\n * `sign_up` is a GA4 recommended event and every other funnel step on\n * this site already reports one, so the account door was the single\n * hole in a funnel the operator can otherwise follow end to end.\n *\n * AWAITED, because the line below tears the document down. Delivery on\n * this surface is a synchronous `window.gtag` call today, so the await\n * costs a microtask — it is written this way because the property that\n * makes a bare call safe is invisible from here, and the day a host\n * registers an async transport the event would start disappearing on\n * exactly the branch that navigates.\n */\n await trackEventBeforeNavigation('sign_up', { method: 'password' })\n memberNavigation.assign(\n continuePathFromLocation(continueFallback || '/'),\n )\n } finally {\n setBusy(false)\n }\n }, [\n hostId,\n busy,\n displayName,\n email,\n password,\n marketingConsent,\n continueFallback,\n siteFetch,\n ])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\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 },\n ...nodeSx,\n ]}\n >\n {'Member sign-up — the create-account form renders here'}\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest} sx={[{ maxWidth: 420 }, ...nodeSx]}>\n {heading ? (\n <Typography variant=\"h5\" gutterBottom>\n {heading}\n </Typography>\n ) : null}\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\n <TextField\n label=\"Name\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n size=\"small\"\n />\n <TextField\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n />\n <TextField\n label=\"Password\"\n type=\"password\"\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n size=\"small\"\n helperText=\"At least 8 characters\"\n />\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={marketingConsent}\n onChange={(event) =>\n setMarketingConsent(event.target.checked)\n }\n />\n }\n label=\"Email me about offers and updates\"\n />\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !email || password.length < 8}\n onClick={handleSignup}\n >\n {busy ? 'Working…' : 'Create account'}\n </Button>\n <Typography variant=\"body2\">\n <Link href={signinPath || '/signin'} underline=\"hover\">\n {'Already a member? Sign in'}\n </Link>\n </Typography>\n </Box>\n </Box>\n )\n },\n)\nMemberSignup.displayName = 'AglynMemberSignup'\n\nexport const schema: Aglyn.ComponentSchema<MemberSignupProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Member sign-up',\n description: 'A create-account form for site membership.',\n category: Aglyn.ComponentCategory.MEMBERS,\n icon: { path: mdiAccountPlusOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'heading',\n label: 'Heading',\n description:\n 'The title above the sign-up fields. Say what joining gets them; ' +\n 'blank renders no heading at all.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'signinPath',\n label: 'Sign-in path',\n description: 'Where \"Already a member?\" points (default /signin).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'continueFallback',\n label: 'After sign-up',\n description:\n 'Where a new member goes once the account EXISTS, when nothing sent ' +\n 'them here. A `?continue=` on the URL always wins. Point it at a ' +\n 'welcome or onboarding page rather than the home page if you have ' +\n 'one.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Member sign-up',\n pluginId: BUNDLE_ID,\n description: 'Create-account form for site membership',\n category: Aglyn.ComponentCategory.MEMBERS,\n icon: { path: mdiAccountPlusOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { heading: 'Create your account' },\n },\n },\n]\n\nexport default MemberSignup\n"],"names":["Aglyn","trackEventBeforeNavigation","utmTouchField","mdiAccountPlusOutline","Alert","Box","Button","Checkbox","FormControlLabel","Link","TextField","Typography","forwardRef","useCallback","useState","BUNDLE_ID","continuePathFromLocation","memberNavigation","generatePresetId","ID","MemberSignup","props","ref","heading","signinPath","continueFallback","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","displayName","setDisplayName","email","setEmail","password","setPassword","marketingConsent","setMarketingConsent","error","setError","busy","setBusy","handleSignup","response","method","headers","body","JSON","stringify","ok","payload","json","catch","String","undefined","assign","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","maxWidth","variant","gutterBottom","display","flexDirection","gap","label","value","onChange","event","target","size","type","helperText","control","checked","severity","disabled","length","onClick","href","underline","schema","$id","pluginId","description","category","ComponentCategory","MEMBERS","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","component","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
17
  * See the License for the specific language governing permissions and
18
18
  * limitations under the License.
19
19
  */ import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
20
- import { campaignTouchField } from "@aglyn/aglyn/app-utils/campaign-touch";
20
+ import { utmTouchField } from "@aglyn/aglyn/app-utils/utm-touch";
21
21
  import * as Aglyn from "@aglyn/aglyn";
22
22
  import { mdiEmailOutline } from "@aglyn/shared-data-mdi";
23
23
  import Box from "@mui/material/Box";
@@ -62,7 +62,7 @@ export const ID = 'newsletter-signup';
62
62
  email
63
63
  }, listId ? {
64
64
  listId
65
- } : {}, campaignTouchField()))
65
+ } : {}, utmTouchField()))
66
66
  });
67
67
  // GA4 lead conversion (AGL-1561) — AGL-301's subscribe element is a
68
68
  // form completion in every sense that matters, and it shares nothing
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/newsletter-signup.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 { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { campaignTouchField } from '@aglyn/aglyn/app-utils/campaign-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailOutline } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, 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 = 'newsletter-signup'\n\nexport interface NewsletterSignupProps {\n heading?: string\n buttonLabel?: string\n successText?: string\n /**\n * Enrolls the subscriber into this list's members (AGL-2499), in\n * addition to the always-on contacts upsert. This is the id shown on the\n * Audiences page's list settings — copy it from there. Left blank, the\n * block behaves exactly as before: contacts only.\n */\n listId?: string\n}\n\n/**\n * Newsletter signup block (AGL-301): consented email capture straight\n * into the contacts CRM (source `newsletter`), ready for email-campaign\n * audiences.\n */\nconst NewsletterSignup = forwardRef<HTMLDivElement, NewsletterSignupProps>(\n (props, ref) => {\n const { heading, buttonLabel, successText, listId, ...rest } = 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 [email, setEmail] = useState('')\n const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>(\n 'idle',\n )\n\n const handleSubscribe = async () => {\n if (!hostId || state === 'busy') return\n setState('busy')\n try {\n const response = await siteFetch('/api/commerce/newsletter', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n email,\n ...(listId ? { listId } : {}),\n // The campaign this visitor came from, when they came from one.\n ...campaignTouchField(),\n }),\n })\n // GA4 lead conversion (AGL-1561) — AGL-301's subscribe element is a\n // form completion in every sense that matters, and it shares nothing\n // with the generic `Form` element, so it needs its own call. Only on\n // `response.ok`: a failed subscribe is not a lead. The email is\n // deliberately absent from the payload.\n if (response.ok) {\n trackEvent('generate_lead', {\n form_name: 'Newsletter',\n form_location: window.location.pathname,\n })\n }\n setState(response.ok ? 'done' : 'error')\n } catch {\n setState('error')\n }\n }\n\n if (state === 'done') {\n return (\n <Box ref={ref} {...rest} sx={[{ py: 1 }, ...nodeSx]}>\n <Typography variant=\"body2\">\n {successText || 'You’re on the list — thanks!'}\n </Typography>\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest}>\n {heading ? (\n // `component=\"p\"` because this introduces the field below it and is\n // not a section heading (AGL-2486). MUI's `defaultVariantMapping`\n // sends `subtitle1` to `<h6>`, so without this the signup block puts\n // a level-6 heading into the outline of every page it appears on. On\n // a blog listing the nearest preceding heading is an `h3`, which\n // makes it a skipped level and a `heading-order` failure.\n //\n // It is also not the field's label — see the `aria-label` below,\n // which is the input's only accessible name.\n <Typography component=\"p\" variant=\"subtitle1\" gutterBottom>\n {heading}\n </Typography>\n ) : null}\n <Box sx={{ display: 'flex', gap: 1 }}>\n <TextField\n type=\"email\"\n placeholder=\"you@example.com\"\n // The field's only accessible name (AGL-2392). `heading` is\n // optional and is a plain `subtitle1` above the row, never a\n // `<label>`, so without this the input ships nameless — which is\n // what aglyn.com/blog does today. A placeholder is not a name:\n // assistive tech is not required to announce it, and it vanishes\n // the moment the visitor types.\n slotProps={{ htmlInput: { 'aria-label': 'Email address' } }}\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n sx={{ flex: 1, maxWidth: 320 }}\n error={state === 'error'}\n helperText={state === 'error' ? 'Enter a valid email' : undefined}\n />\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!hostId || state === 'busy' || !email.trim()}\n onClick={handleSubscribe}\n >\n {state === 'busy' ? '…' : buttonLabel || 'Subscribe'}\n </Button>\n </Box>\n </Box>\n )\n },\n)\nNewsletterSignup.displayName = 'AglynNewsletterSignup'\n\nexport const schema: Aglyn.ComponentSchema<NewsletterSignupProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Newsletter signup',\n description: 'A consented email capture that writes straight into contacts.',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiEmailOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'heading',\n label: 'Heading',\n description: 'Shown above the field.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'buttonLabel',\n label: 'Button label',\n description: 'Defaults to \"Subscribe\".',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'successText',\n label: 'Success text',\n description: 'Shown after subscribing.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'listId',\n label: 'List',\n description:\n 'Optional — enrolls subscribers into this list too (id from Audiences → Lists). Blank keeps the old contacts-only behavior.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Newsletter signup',\n pluginId: BUNDLE_ID,\n description: 'Consented email capture into contacts',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiEmailOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { heading: 'Get updates and offers' },\n },\n },\n]\n\nexport default NewsletterSignup\n"],"names":["trackEvent","campaignTouchField","Aglyn","mdiEmailOutline","Box","Button","TextField","Typography","forwardRef","useState","BUNDLE_ID","generatePresetId","ID","NewsletterSignup","props","ref","heading","buttonLabel","successText","listId","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","email","setEmail","state","setState","handleSubscribe","response","method","headers","body","JSON","stringify","ok","form_name","form_location","window","location","pathname","sx","py","variant","component","gutterBottom","display","gap","type","placeholder","slotProps","htmlInput","value","onChange","event","target","size","flex","maxWidth","error","helperText","undefined","color","disabled","trim","onClick","displayName","schema","$id","pluginId","description","category","ComponentCategory","COMMERCE","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","label","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/newsletter-signup.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 { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailOutline } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, 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 = 'newsletter-signup'\n\nexport interface NewsletterSignupProps {\n heading?: string\n buttonLabel?: string\n successText?: string\n /**\n * Enrolls the subscriber into this list's members (AGL-2499), in\n * addition to the always-on contacts upsert. This is the id shown on the\n * Audiences page's list settings — copy it from there. Left blank, the\n * block behaves exactly as before: contacts only.\n */\n listId?: string\n}\n\n/**\n * Newsletter signup block (AGL-301): consented email capture straight\n * into the contacts CRM (source `newsletter`), ready for email-campaign\n * audiences.\n */\nconst NewsletterSignup = forwardRef<HTMLDivElement, NewsletterSignupProps>(\n (props, ref) => {\n const { heading, buttonLabel, successText, listId, ...rest } = 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 [email, setEmail] = useState('')\n const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>(\n 'idle',\n )\n\n const handleSubscribe = async () => {\n if (!hostId || state === 'busy') return\n setState('busy')\n try {\n const response = await siteFetch('/api/commerce/newsletter', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n email,\n ...(listId ? { listId } : {}),\n // The campaign this visitor came from, when they came from one.\n ...utmTouchField(),\n }),\n })\n // GA4 lead conversion (AGL-1561) — AGL-301's subscribe element is a\n // form completion in every sense that matters, and it shares nothing\n // with the generic `Form` element, so it needs its own call. Only on\n // `response.ok`: a failed subscribe is not a lead. The email is\n // deliberately absent from the payload.\n if (response.ok) {\n trackEvent('generate_lead', {\n form_name: 'Newsletter',\n form_location: window.location.pathname,\n })\n }\n setState(response.ok ? 'done' : 'error')\n } catch {\n setState('error')\n }\n }\n\n if (state === 'done') {\n return (\n <Box ref={ref} {...rest} sx={[{ py: 1 }, ...nodeSx]}>\n <Typography variant=\"body2\">\n {successText || 'You’re on the list — thanks!'}\n </Typography>\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest}>\n {heading ? (\n // `component=\"p\"` because this introduces the field below it and is\n // not a section heading (AGL-2486). MUI's `defaultVariantMapping`\n // sends `subtitle1` to `<h6>`, so without this the signup block puts\n // a level-6 heading into the outline of every page it appears on. On\n // a blog listing the nearest preceding heading is an `h3`, which\n // makes it a skipped level and a `heading-order` failure.\n //\n // It is also not the field's label — see the `aria-label` below,\n // which is the input's only accessible name.\n <Typography component=\"p\" variant=\"subtitle1\" gutterBottom>\n {heading}\n </Typography>\n ) : null}\n <Box sx={{ display: 'flex', gap: 1 }}>\n <TextField\n type=\"email\"\n placeholder=\"you@example.com\"\n // The field's only accessible name (AGL-2392). `heading` is\n // optional and is a plain `subtitle1` above the row, never a\n // `<label>`, so without this the input ships nameless — which is\n // what aglyn.com/blog does today. A placeholder is not a name:\n // assistive tech is not required to announce it, and it vanishes\n // the moment the visitor types.\n slotProps={{ htmlInput: { 'aria-label': 'Email address' } }}\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n sx={{ flex: 1, maxWidth: 320 }}\n error={state === 'error'}\n helperText={state === 'error' ? 'Enter a valid email' : undefined}\n />\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!hostId || state === 'busy' || !email.trim()}\n onClick={handleSubscribe}\n >\n {state === 'busy' ? '…' : buttonLabel || 'Subscribe'}\n </Button>\n </Box>\n </Box>\n )\n },\n)\nNewsletterSignup.displayName = 'AglynNewsletterSignup'\n\nexport const schema: Aglyn.ComponentSchema<NewsletterSignupProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Newsletter signup',\n description: 'A consented email capture that writes straight into contacts.',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiEmailOutline.path, sx: { color: '#2e7d32' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'heading',\n label: 'Heading',\n description: 'Shown above the field.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'buttonLabel',\n label: 'Button label',\n description: 'Defaults to \"Subscribe\".',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'successText',\n label: 'Success text',\n description: 'Shown after subscribing.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'listId',\n label: 'List',\n description:\n 'Optional — enrolls subscribers into this list too (id from Audiences → Lists). Blank keeps the old contacts-only behavior.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Newsletter signup',\n pluginId: BUNDLE_ID,\n description: 'Consented email capture into contacts',\n category: Aglyn.ComponentCategory.COMMERCE,\n icon: { path: mdiEmailOutline.path, sx: { color: '#2e7d32' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { heading: 'Get updates and offers' },\n },\n },\n]\n\nexport default NewsletterSignup\n"],"names":["trackEvent","utmTouchField","Aglyn","mdiEmailOutline","Box","Button","TextField","Typography","forwardRef","useState","BUNDLE_ID","generatePresetId","ID","NewsletterSignup","props","ref","heading","buttonLabel","successText","listId","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","email","setEmail","state","setState","handleSubscribe","response","method","headers","body","JSON","stringify","ok","form_name","form_location","window","location","pathname","sx","py","variant","component","gutterBottom","display","gap","type","placeholder","slotProps","htmlInput","value","onChange","event","target","size","flex","maxWidth","error","helperText","undefined","color","disabled","trim","onClick","displayName","schema","$id","pluginId","description","category","ComponentCategory","COMMERCE","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","label","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
@@ -44,7 +44,9 @@ export const COMMERCE_TAX_NOT_REVERSED = {
44
44
  tier: 'must',
45
45
  category: 'payments',
46
46
  title: 'Sales tax not reversed on invoice {{invoiceId}}',
47
- body: '{{amount}} of sales tax on invoice {{invoiceId}} (site {{hostId}}) was not pulled back from the merchant: {{reason}}.',
47
+ // `{{site}}` is the site's name and id (AGL-3432), so staff need not look
48
+ // it up; the last sentence is the consequence the description states.
49
+ body: '{{amount}} of sales tax on invoice {{invoiceId}} for site {{site}} was not pulled back from the merchant: {{reason}}. The platform files and remits that tax, so it may owe tax it does not hold.',
48
50
  delivery: 'immediate',
49
51
  dedupeWindowMinutes: 7 * DAY,
50
52
  defaultEnabled: true
@@ -57,7 +59,7 @@ export const COMMERCE_SELLER_SHARE_NOT_REVERSED = {
57
59
  tier: 'must',
58
60
  category: 'payments',
59
61
  title: 'Seller share not reversed on dispute {{disputeId}}',
60
- body: 'The lost dispute {{disputeId}} on order {{orderId}} (site {{hostId}}) cost {{amount}}, and the merchant’s share was not reversed: {{reason}}.',
62
+ body: 'The lost dispute {{disputeId}} on order {{orderId}} from site {{site}} cost {{amount}}, and the merchant’s share was not reversed: {{reason}}. The platform is out that share until it is recovered by hand in Stripe.',
61
63
  delivery: 'immediate',
62
64
  dedupeWindowMinutes: 30 * DAY,
63
65
  defaultEnabled: true
@@ -70,7 +72,7 @@ export const COMMERCE_FEE_REPRICE_REFUSED = {
70
72
  tier: 'must',
71
73
  category: 'payments',
72
74
  title: 'Platform fee not corrected on {{subject}}',
73
- body: 'The platform fee on {{subject}} (site {{hostId}}) could not be corrected: {{reason}}.',
75
+ body: 'The platform fee on {{subject}} for site {{site}} could not be corrected: {{reason}}. Until it is, the merchant is charged a fee its plan does not set.',
74
76
  delivery: 'immediate',
75
77
  dedupeWindowMinutes: 7 * DAY,
76
78
  defaultEnabled: true