@aglyn/plugins-commerce 1.0.0-beta.250 → 1.0.0-beta.252

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 (52) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/commerce-console-page.d.ts +1 -1
  3. package/src/lib/components/commerce-console-page.js +139 -175
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/commerce-console-sections.d.ts +30 -2
  6. package/src/lib/components/commerce-console-sections.js +82 -7
  7. package/src/lib/components/commerce-console-sections.js.map +1 -1
  8. package/src/lib/components/console/discounts-card.component.js +36 -3
  9. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-coupons-card.component.js +1 -1
  11. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  12. package/src/lib/components/console/pos/pos-pairing.component.d.ts +59 -0
  13. package/src/lib/components/console/pos/pos-pairing.component.js +174 -0
  14. package/src/lib/components/console/pos/pos-pairing.component.js.map +1 -0
  15. package/src/lib/components/console/pos/use-pos-display.d.ts +0 -8
  16. package/src/lib/components/console/pos/use-pos-display.js +2 -16
  17. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  18. package/src/lib/components/console/pos-devices-card.component.d.ts +12 -1
  19. package/src/lib/components/console/pos-devices-card.component.js +132 -59
  20. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  21. package/src/lib/components/console/pos-page.component.d.ts +1 -1
  22. package/src/lib/components/console/pos-page.component.js +37 -86
  23. package/src/lib/components/console/pos-page.component.js.map +1 -1
  24. package/src/lib/components/console/registers-card.component.js +13 -0
  25. package/src/lib/components/console/registers-card.component.js.map +1 -1
  26. package/src/lib/components/console/shipping-settings-card.component.js +6 -11
  27. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  28. package/src/lib/components/console/store-zones.d.ts +15 -0
  29. package/src/lib/components/console/store-zones.js +36 -4
  30. package/src/lib/components/console/store-zones.js.map +1 -1
  31. package/src/lib/components/console/suppliers-card.component.js +2 -2
  32. package/src/lib/components/console/suppliers-card.component.js.map +1 -1
  33. package/src/lib/model/commerce-record-routes.js +10 -3
  34. package/src/lib/model/commerce-record-routes.js.map +1 -1
  35. package/src/lib/model/commerce-shipping.d.ts +16 -0
  36. package/src/lib/model/commerce-shipping.js +21 -0
  37. package/src/lib/model/commerce-shipping.js.map +1 -1
  38. package/src/lib/model/commerce-tax-decision.js +2 -2
  39. package/src/lib/model/commerce-tax-decision.js.map +1 -1
  40. package/src/lib/plugin.js +4 -1
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +13 -0
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/order-webhooks.js +1 -1
  45. package/src/lib/server/order-webhooks.js.map +1 -1
  46. package/src/lib/server/shippingeasy.js +1 -1
  47. package/src/lib/server/shippingeasy.js.map +1 -1
  48. package/src/lib/server/store-setup-ai-capability.js +10 -2
  49. package/src/lib/server/store-setup-ai-capability.js.map +1 -1
  50. package/src/lib/server/supplier-routing.d.ts +86 -0
  51. package/src/lib/server/supplier-routing.js +175 -0
  52. package/src/lib/server/supplier-routing.js.map +1 -0
@@ -0,0 +1,174 @@
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 { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
20
+ import { useCallback, useState } from "react";
21
+ import * as CommerceModel from "../../../model/index.js";
22
+ import { posDisplayCall, PosRequestError } from "./pos-api.js";
23
+ /**
24
+ * A fresh six-digit code that pairs a tablet to one register, as a customer
25
+ * display or a self-service kiosk (AGL-3608, AGL-3623). The server holds the
26
+ * gate: the register's role and `managePos` checks and the plan's `pos`.
27
+ */ export function requestPosPairingCode(user, hostId, registerId, mode = 'display') {
28
+ return posDisplayCall(user, {
29
+ action: 'pairing-code',
30
+ hostId,
31
+ registerId,
32
+ mode
33
+ });
34
+ }
35
+ /** The address a tablet opens to enter the code. */ export function posPairingAddress(mode) {
36
+ const origin = typeof window === 'undefined' ? '' : window.location.origin;
37
+ return `${origin}/kiosk/commerce/${mode === 'kiosk' ? 'pos-kiosk' : 'pos-display'}`;
38
+ }
39
+ const TTL_MINUTES = Math.round(CommerceModel.POS_DISPLAY_PAIRING_TTL_MS / 60000);
40
+ /** The code, the address to enter it at, and the display-or-kiosk choice. */ export function PosPairingDialog({ pairing, onModeChange, onClose }) {
41
+ var _ref;
42
+ const mode = (_ref = pairing == null ? void 0 : pairing.mode) != null ? _ref : 'display';
43
+ return /*#__PURE__*/ _jsxs(Dialog, {
44
+ open: Boolean(pairing),
45
+ onClose: onClose,
46
+ maxWidth: "xs",
47
+ fullWidth: true,
48
+ children: [
49
+ /*#__PURE__*/ _jsxs(DialogTitle, {
50
+ children: [
51
+ mode === 'kiosk' ? 'Pair a self-service kiosk' : 'Pair a customer display',
52
+ (pairing == null ? void 0 : pairing.registerName) ? /*#__PURE__*/ _jsx(Typography, {
53
+ component: "span",
54
+ variant: "body2",
55
+ color: "text.secondary",
56
+ sx: {
57
+ display: 'block'
58
+ },
59
+ children: `For ${pairing.registerName}`
60
+ }) : null
61
+ ]
62
+ }),
63
+ /*#__PURE__*/ _jsxs(DialogContent, {
64
+ sx: {
65
+ display: 'flex',
66
+ flexDirection: 'column',
67
+ gap: 2
68
+ },
69
+ children: [
70
+ /*#__PURE__*/ _jsxs(ToggleButtonGroup, {
71
+ exclusive: true,
72
+ fullWidth: true,
73
+ size: "small",
74
+ value: mode,
75
+ onChange: (_event, next)=>{
76
+ if (next && next !== mode) onModeChange(next);
77
+ },
78
+ sx: {
79
+ mt: 1
80
+ },
81
+ children: [
82
+ /*#__PURE__*/ _jsx(ToggleButton, {
83
+ value: "display",
84
+ children: 'Customer display'
85
+ }),
86
+ /*#__PURE__*/ _jsx(ToggleButton, {
87
+ value: "kiosk",
88
+ children: 'Self-service kiosk'
89
+ })
90
+ ]
91
+ }),
92
+ /*#__PURE__*/ _jsx(Typography, {
93
+ variant: "body2",
94
+ color: "text.secondary",
95
+ children: (mode === 'kiosk' ? 'On the tablet customers order from, open this address and enter the code. ' : 'On the tablet facing your customer, open this address and enter the code. ') + `The code works once and expires in ${TTL_MINUTES} minutes.`
96
+ }),
97
+ /*#__PURE__*/ _jsx(Typography, {
98
+ variant: "body1",
99
+ sx: {
100
+ wordBreak: 'break-all'
101
+ },
102
+ children: posPairingAddress(mode)
103
+ }),
104
+ /*#__PURE__*/ _jsx(Typography, {
105
+ variant: "h3",
106
+ component: "p",
107
+ sx: {
108
+ textAlign: 'center',
109
+ letterSpacing: 8
110
+ },
111
+ children: pairing == null ? void 0 : pairing.code
112
+ })
113
+ ]
114
+ }),
115
+ /*#__PURE__*/ _jsx(DialogActions, {
116
+ children: /*#__PURE__*/ _jsx(Button, {
117
+ onClick: onClose,
118
+ children: 'Done'
119
+ })
120
+ })
121
+ ]
122
+ });
123
+ }
124
+ PosPairingDialog.displayName = 'PosPairingDialog';
125
+ /**
126
+ * The one pairing flow, for the register and the POS devices card alike:
127
+ * `open(registerId)` asks for a code and shows it; switching between
128
+ * customer display and kiosk asks for a code of that kind. Render `dialog`.
129
+ */ export function usePosPairing({ user, hostId, onError }) {
130
+ const [pairing, setPairing] = useState(null);
131
+ const open = useCallback(async (registerId, mode = 'display', registerName)=>{
132
+ if (!user || !registerId) return;
133
+ try {
134
+ const answer = await requestPosPairingCode(user, hostId, registerId, mode);
135
+ setPairing(_extends({}, answer, {
136
+ registerId,
137
+ mode
138
+ }, registerName ? {
139
+ registerName
140
+ } : {}));
141
+ } catch (error) {
142
+ onError(error instanceof PosRequestError ? error.message : 'Could not make a code');
143
+ }
144
+ }, [
145
+ user,
146
+ hostId,
147
+ onError
148
+ ]);
149
+ const close = useCallback(()=>setPairing(null), []);
150
+ const dialog = /*#__PURE__*/ _jsx(PosPairingDialog, {
151
+ pairing: pairing,
152
+ onClose: close,
153
+ onModeChange: (mode)=>{
154
+ if (pairing) void open(pairing.registerId, mode, pairing.registerName);
155
+ }
156
+ });
157
+ return {
158
+ pairing,
159
+ open,
160
+ close,
161
+ dialog
162
+ };
163
+ }
164
+ /** The element id the POS registers card carries, for a link that lands on it. */ export const POS_REGISTERS_ANCHOR = 'pos-registers';
165
+ /**
166
+ * Where a register is created — the POS registers card, on Products → Point
167
+ * of sale (`…/products/pos`, AGL-3713) — from the register page's own console
168
+ * path (`…/pos`). Undefined when that path is not the register's.
169
+ */ export function posRegistersSettingsHref(posBasePath) {
170
+ if (!posBasePath || !/\/pos\/?$/.test(posBasePath)) return undefined;
171
+ return `${posBasePath.replace(/\/pos\/?$/, '/products/pos')}#${POS_REGISTERS_ANCHOR}`;
172
+ }
173
+
174
+ //# sourceMappingURL=pos-pairing.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../../libs/plugins/commerce/src/lib/components/console/pos/pos-pairing.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 {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { useCallback, useState, type ReactNode } from 'react'\nimport * as CommerceModel from '../../../model'\nimport { posDisplayCall, PosRequestError } from './pos-api'\n\ntype User = Parameters<typeof posDisplayCall>[0]\n\n/** A code on screen, and the register and kind of device it pairs. */\nexport interface PosPairingState {\n registerId: string\n registerName?: string\n mode: CommerceModel.PosDeviceMode\n code: string\n expiresAtMs: number\n}\n\n/**\n * A fresh six-digit code that pairs a tablet to one register, as a customer\n * display or a self-service kiosk (AGL-3608, AGL-3623). The server holds the\n * gate: the register's role and `managePos` checks and the plan's `pos`.\n */\nexport function requestPosPairingCode(\n user: User,\n hostId: string,\n registerId: string,\n mode: CommerceModel.PosDeviceMode = 'display',\n): Promise<{ code: string; expiresAtMs: number }> {\n return posDisplayCall<{ code: string; expiresAtMs: number }>(user, {\n action: 'pairing-code',\n hostId,\n registerId,\n mode,\n })\n}\n\n/** The address a tablet opens to enter the code. */\nexport function posPairingAddress(mode: CommerceModel.PosDeviceMode): string {\n const origin = typeof window === 'undefined' ? '' : window.location.origin\n return `${origin}/kiosk/commerce/${mode === 'kiosk' ? 'pos-kiosk' : 'pos-display'}`\n}\n\nconst TTL_MINUTES = Math.round(CommerceModel.POS_DISPLAY_PAIRING_TTL_MS / 60_000)\n\nexport interface PosPairingDialogProps {\n pairing: PosPairingState | null\n onModeChange: (mode: CommerceModel.PosDeviceMode) => void\n onClose: () => void\n}\n\n/** The code, the address to enter it at, and the display-or-kiosk choice. */\nexport function PosPairingDialog({ pairing, onModeChange, onClose }: PosPairingDialogProps) {\n const mode = pairing?.mode ?? 'display'\n return (\n <Dialog open={Boolean(pairing)} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>\n {mode === 'kiosk' ? 'Pair a self-service kiosk' : 'Pair a customer display'}\n {pairing?.registerName ? (\n <Typography component=\"span\" variant=\"body2\" color=\"text.secondary\" sx={{ display: 'block' }}>\n {`For ${pairing.registerName}`}\n </Typography>\n ) : null}\n </DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <ToggleButtonGroup\n exclusive\n fullWidth\n size=\"small\"\n value={mode}\n onChange={(_event, next: CommerceModel.PosDeviceMode | null) => {\n if (next && next !== mode) onModeChange(next)\n }}\n sx={{ mt: 1 }}\n >\n <ToggleButton value=\"display\">{'Customer display'}</ToggleButton>\n <ToggleButton value=\"kiosk\">{'Self-service kiosk'}</ToggleButton>\n </ToggleButtonGroup>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(mode === 'kiosk'\n ? 'On the tablet customers order from, open this address and enter the code. '\n : 'On the tablet facing your customer, open this address and enter the code. ') +\n `The code works once and expires in ${TTL_MINUTES} minutes.`}\n </Typography>\n <Typography variant=\"body1\" sx={{ wordBreak: 'break-all' }}>\n {posPairingAddress(mode)}\n </Typography>\n <Typography variant=\"h3\" component=\"p\" sx={{ textAlign: 'center', letterSpacing: 8 }}>\n {pairing?.code}\n </Typography>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Done'}</Button>\n </DialogActions>\n </Dialog>\n )\n}\nPosPairingDialog.displayName = 'PosPairingDialog'\n\nexport interface UsePosPairingOptions {\n user: User | null | undefined\n hostId: string\n /** Told the reason a code could not be made. */\n onError: (message: string) => void\n}\n\n/**\n * The one pairing flow, for the register and the POS devices card alike:\n * `open(registerId)` asks for a code and shows it; switching between\n * customer display and kiosk asks for a code of that kind. Render `dialog`.\n */\nexport function usePosPairing({ user, hostId, onError }: UsePosPairingOptions): {\n pairing: PosPairingState | null\n open: (registerId: string, mode?: CommerceModel.PosDeviceMode, registerName?: string) => Promise<void>\n close: () => void\n dialog: ReactNode\n} {\n const [pairing, setPairing] = useState<PosPairingState | null>(null)\n\n const open = useCallback(\n async (registerId: string, mode: CommerceModel.PosDeviceMode = 'display', registerName?: string) => {\n if (!user || !registerId) return\n try {\n const answer = await requestPosPairingCode(user, hostId, registerId, mode)\n setPairing({ ...answer, registerId, mode, ...(registerName ? { registerName } : {}) })\n } catch (error) {\n onError(error instanceof PosRequestError ? error.message : 'Could not make a code')\n }\n },\n [user, hostId, onError],\n )\n const close = useCallback(() => setPairing(null), [])\n\n const dialog = (\n <PosPairingDialog\n pairing={pairing}\n onClose={close}\n onModeChange={(mode) => {\n if (pairing) void open(pairing.registerId, mode, pairing.registerName)\n }}\n />\n )\n return { pairing, open, close, dialog }\n}\n\n/** The element id the POS registers card carries, for a link that lands on it. */\nexport const POS_REGISTERS_ANCHOR = 'pos-registers'\n\n/**\n * Where a register is created — the POS registers card, on Products → Point\n * of sale (`…/products/pos`, AGL-3713) — from the register page's own console\n * path (`…/pos`). Undefined when that path is not the register's.\n */\nexport function posRegistersSettingsHref(posBasePath: string | undefined): string | undefined {\n if (!posBasePath || !/\\/pos\\/?$/.test(posBasePath)) return undefined\n return `${posBasePath.replace(/\\/pos\\/?$/, '/products/pos')}#${POS_REGISTERS_ANCHOR}`\n}\n"],"names":["Button","Dialog","DialogActions","DialogContent","DialogTitle","ToggleButton","ToggleButtonGroup","Typography","useCallback","useState","CommerceModel","posDisplayCall","PosRequestError","requestPosPairingCode","user","hostId","registerId","mode","action","posPairingAddress","origin","window","location","TTL_MINUTES","Math","round","POS_DISPLAY_PAIRING_TTL_MS","PosPairingDialog","pairing","onModeChange","onClose","open","Boolean","maxWidth","fullWidth","registerName","component","variant","color","sx","display","flexDirection","gap","exclusive","size","value","onChange","_event","next","mt","wordBreak","textAlign","letterSpacing","code","onClick","displayName","usePosPairing","onError","setPairing","answer","error","message","close","dialog","POS_REGISTERS_ANCHOR","posRegistersSettingsHref","posBasePath","test","undefined","replace"],"mappings":"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"}
@@ -27,14 +27,6 @@ export interface PosDisplayControl {
27
27
  */
28
28
  ask: (state: Omit<CommerceModel.PosDisplayState, 'updatedAtMs' | 'promptId'>) => Promise<CommerceModel.PosDisplayResponse | null>;
29
29
  cancelAsk: () => void;
30
- /**
31
- * A fresh six-digit pairing code for a new customer display, or for a
32
- * self-service kiosk (AGL-3623).
33
- */
34
- pairingCode: (mode?: CommerceModel.PosDeviceMode) => Promise<{
35
- code: string;
36
- expiresAtMs: number;
37
- }>;
38
30
  }
39
31
  /**
40
32
  * The register's half of the customer display (AGL-3608). It writes the
@@ -135,27 +135,13 @@ import { newAttemptKey, posDisplayCall } from "./pos-api.js";
135
135
  hostId,
136
136
  registerId
137
137
  ]);
138
- const pairingCode = useCallback(async (mode = 'display')=>{
139
- if (!user) throw new Error('Not signed in');
140
- const result = await posDisplayCall(user, {
141
- action: 'pairing-code',
142
- hostId,
143
- registerId,
144
- mode
145
- });
146
- return result;
147
- }, [
148
- user,
149
- hostId,
150
- registerId
151
- ]);
138
+ // Pairing a new display is `usePosPairing` (AGL-3712), shared with the devices card.
152
139
  return {
153
140
  connected,
154
141
  asking,
155
142
  show,
156
143
  ask,
157
- cancelAsk,
158
- pairingCode
144
+ cancelAsk
159
145
  };
160
146
  }
161
147
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../../libs/plugins/commerce/src/lib/components/console/pos/use-pos-display.ts"],"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 { useCallback, useEffect, useRef, useState } from 'react'\nimport type * as CommerceModel from '../../../model'\nimport { newAttemptKey, posDisplayCall } from './pos-api'\n\ntype User = Parameters<typeof posDisplayCall>[0]\n\n/**\n * The tip the customer screen shows beside the sale (AGL-3608): the tips\n * already on its payments, plus one chosen for the payment about to be taken\n * (on the display or by the cashier), which the ledger does not hold until\n * that payment is recorded. Without the second part the screen read the\n * sale's total right after the customer added a tip, short by that tip.\n */\nexport function posDisplayTipCents(\n sale: { status: string; tipCents: number } | null | undefined,\n pendingTipCents: number,\n): number {\n if (!sale) return 0\n const pending = sale.status === 'paid' ? 0 : Math.max(0, Math.round(pendingTipCents) || 0)\n return Math.max(0, sale.tipCents) + pending\n}\n\n/** How long the register waits for the customer to answer a prompt. */\nexport const POS_DISPLAY_ANSWER_TIMEOUT_MS = 3 * 60 * 1000\n\nexport interface PosDisplayControl {\n /** A display for this register polled within the last two minutes. */\n connected: boolean\n /** The prompt the register is waiting on, if any. */\n asking: CommerceModel.PosDisplayMode | null\n /** Shows something on the display with no answer expected. */\n show: (state: Omit<CommerceModel.PosDisplayState, 'updatedAtMs' | 'promptId'>) => Promise<void>\n /**\n * Asks the customer (a tip or a receipt) and resolves with their answer,\n * or null when the cashier cancels or the customer does not answer.\n */\n ask: (\n state: Omit<CommerceModel.PosDisplayState, 'updatedAtMs' | 'promptId'>,\n ) => Promise<CommerceModel.PosDisplayResponse | null>\n cancelAsk: () => void\n /**\n * A fresh six-digit pairing code for a new customer display, or for a\n * self-service kiosk (AGL-3623).\n */\n pairingCode: (mode?: CommerceModel.PosDeviceMode) => Promise<{ code: string; expiresAtMs: number }>\n}\n\n/**\n * The register's half of the customer display (AGL-3608). It writes the\n * display's state through `commerce/pos-display` and reads answers back the\n * same way; nothing here talks to Firestore, because the display state is\n * server-only (it can hold a customer's typed email address).\n */\nexport function usePosDisplay(\n user: User | null | undefined,\n hostId: string,\n registerId: string,\n): PosDisplayControl {\n const [connected, setConnected] = useState(false)\n const [asking, setAsking] = useState<CommerceModel.PosDisplayMode | null>(null)\n const cancelRef = useRef<(() => void) | null>(null)\n // The poll reads the latest session through a ref and is keyed on the uid,\n // so a refreshed user object does not restart it.\n const userRef = useRef(user)\n userRef.current = user\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!uid || !registerId) return undefined\n let active = true\n const check = async () => {\n const signedIn = userRef.current\n if (!signedIn) return\n try {\n const result = await posDisplayCall<{ connected: boolean }>(\n signedIn,\n { action: 'state', hostId, registerId },\n 'GET',\n )\n if (active) setConnected(Boolean(result.connected))\n } catch {\n if (active) setConnected(false)\n }\n }\n void check()\n const timer = setInterval(check, 15_000)\n return () => {\n active = false\n clearInterval(timer)\n }\n }, [uid, hostId, registerId])\n\n const show = useCallback<PosDisplayControl['show']>(\n async (state) => {\n if (!user || !registerId || !connected) return\n await posDisplayCall(user, { action: 'push', hostId, registerId, state }).catch(() => undefined)\n },\n [user, hostId, registerId, connected],\n )\n\n const cancelAsk = useCallback(() => {\n cancelRef.current?.()\n }, [])\n\n const ask = useCallback<PosDisplayControl['ask']>(\n async (state) => {\n if (!user || !registerId) return null\n cancelRef.current?.()\n const promptId = newAttemptKey().slice(0, 32)\n await posDisplayCall(user, {\n action: 'push',\n hostId,\n registerId,\n state: { ...state, promptId },\n })\n setAsking(state.mode)\n return await new Promise<CommerceModel.PosDisplayResponse | null>((resolve) => {\n let done = false\n const finish = (answer: CommerceModel.PosDisplayResponse | null) => {\n if (done) return\n done = true\n clearInterval(timer)\n clearTimeout(timeout)\n cancelRef.current = null\n setAsking(null)\n resolve(answer)\n }\n const timer = setInterval(async () => {\n try {\n const result = await posDisplayCall<{\n state: CommerceModel.PosDisplayState | null\n }>(user, { action: 'state', hostId, registerId }, 'GET')\n const response = result.state?.response\n if (response && response.promptId === promptId) finish(response)\n } catch {\n // A missed poll is retried on the next tick.\n }\n }, 1000)\n const timeout = setTimeout(() => finish(null), POS_DISPLAY_ANSWER_TIMEOUT_MS)\n cancelRef.current = () => finish(null)\n })\n },\n [user, hostId, registerId],\n )\n\n const pairingCode = useCallback(async (mode: CommerceModel.PosDeviceMode = 'display') => {\n if (!user) throw new Error('Not signed in')\n const result = await posDisplayCall<{ code: string; expiresAtMs: number }>(user, {\n action: 'pairing-code',\n hostId,\n registerId,\n mode,\n })\n return result\n }, [user, hostId, registerId])\n\n return { connected, asking, show, ask, cancelAsk, pairingCode }\n}\n"],"names":["useCallback","useEffect","useRef","useState","newAttemptKey","posDisplayCall","posDisplayTipCents","sale","pendingTipCents","pending","status","Math","max","round","tipCents","POS_DISPLAY_ANSWER_TIMEOUT_MS","usePosDisplay","user","hostId","registerId","connected","setConnected","asking","setAsking","cancelRef","userRef","current","uid","undefined","active","check","signedIn","result","action","Boolean","timer","setInterval","clearInterval","show","state","catch","cancelAsk","ask","promptId","slice","mode","Promise","resolve","done","finish","answer","clearTimeout","timeout","response","setTimeout","pairingCode","Error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../../libs/plugins/commerce/src/lib/components/console/pos/use-pos-display.ts"],"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 { useCallback, useEffect, useRef, useState } from 'react'\nimport type * as CommerceModel from '../../../model'\nimport { newAttemptKey, posDisplayCall } from './pos-api'\n\ntype User = Parameters<typeof posDisplayCall>[0]\n\n/**\n * The tip the customer screen shows beside the sale (AGL-3608): the tips\n * already on its payments, plus one chosen for the payment about to be taken\n * (on the display or by the cashier), which the ledger does not hold until\n * that payment is recorded. Without the second part the screen read the\n * sale's total right after the customer added a tip, short by that tip.\n */\nexport function posDisplayTipCents(\n sale: { status: string; tipCents: number } | null | undefined,\n pendingTipCents: number,\n): number {\n if (!sale) return 0\n const pending = sale.status === 'paid' ? 0 : Math.max(0, Math.round(pendingTipCents) || 0)\n return Math.max(0, sale.tipCents) + pending\n}\n\n/** How long the register waits for the customer to answer a prompt. */\nexport const POS_DISPLAY_ANSWER_TIMEOUT_MS = 3 * 60 * 1000\n\nexport interface PosDisplayControl {\n /** A display for this register polled within the last two minutes. */\n connected: boolean\n /** The prompt the register is waiting on, if any. */\n asking: CommerceModel.PosDisplayMode | null\n /** Shows something on the display with no answer expected. */\n show: (state: Omit<CommerceModel.PosDisplayState, 'updatedAtMs' | 'promptId'>) => Promise<void>\n /**\n * Asks the customer (a tip or a receipt) and resolves with their answer,\n * or null when the cashier cancels or the customer does not answer.\n */\n ask: (\n state: Omit<CommerceModel.PosDisplayState, 'updatedAtMs' | 'promptId'>,\n ) => Promise<CommerceModel.PosDisplayResponse | null>\n cancelAsk: () => void\n}\n\n/**\n * The register's half of the customer display (AGL-3608). It writes the\n * display's state through `commerce/pos-display` and reads answers back the\n * same way; nothing here talks to Firestore, because the display state is\n * server-only (it can hold a customer's typed email address).\n */\nexport function usePosDisplay(\n user: User | null | undefined,\n hostId: string,\n registerId: string,\n): PosDisplayControl {\n const [connected, setConnected] = useState(false)\n const [asking, setAsking] = useState<CommerceModel.PosDisplayMode | null>(null)\n const cancelRef = useRef<(() => void) | null>(null)\n // The poll reads the latest session through a ref and is keyed on the uid,\n // so a refreshed user object does not restart it.\n const userRef = useRef(user)\n userRef.current = user\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!uid || !registerId) return undefined\n let active = true\n const check = async () => {\n const signedIn = userRef.current\n if (!signedIn) return\n try {\n const result = await posDisplayCall<{ connected: boolean }>(\n signedIn,\n { action: 'state', hostId, registerId },\n 'GET',\n )\n if (active) setConnected(Boolean(result.connected))\n } catch {\n if (active) setConnected(false)\n }\n }\n void check()\n const timer = setInterval(check, 15_000)\n return () => {\n active = false\n clearInterval(timer)\n }\n }, [uid, hostId, registerId])\n\n const show = useCallback<PosDisplayControl['show']>(\n async (state) => {\n if (!user || !registerId || !connected) return\n await posDisplayCall(user, { action: 'push', hostId, registerId, state }).catch(() => undefined)\n },\n [user, hostId, registerId, connected],\n )\n\n const cancelAsk = useCallback(() => {\n cancelRef.current?.()\n }, [])\n\n const ask = useCallback<PosDisplayControl['ask']>(\n async (state) => {\n if (!user || !registerId) return null\n cancelRef.current?.()\n const promptId = newAttemptKey().slice(0, 32)\n await posDisplayCall(user, {\n action: 'push',\n hostId,\n registerId,\n state: { ...state, promptId },\n })\n setAsking(state.mode)\n return await new Promise<CommerceModel.PosDisplayResponse | null>((resolve) => {\n let done = false\n const finish = (answer: CommerceModel.PosDisplayResponse | null) => {\n if (done) return\n done = true\n clearInterval(timer)\n clearTimeout(timeout)\n cancelRef.current = null\n setAsking(null)\n resolve(answer)\n }\n const timer = setInterval(async () => {\n try {\n const result = await posDisplayCall<{\n state: CommerceModel.PosDisplayState | null\n }>(user, { action: 'state', hostId, registerId }, 'GET')\n const response = result.state?.response\n if (response && response.promptId === promptId) finish(response)\n } catch {\n // A missed poll is retried on the next tick.\n }\n }, 1000)\n const timeout = setTimeout(() => finish(null), POS_DISPLAY_ANSWER_TIMEOUT_MS)\n cancelRef.current = () => finish(null)\n })\n },\n [user, hostId, registerId],\n )\n\n // Pairing a new display is `usePosPairing` (AGL-3712), shared with the devices card.\n return { connected, asking, show, ask, cancelAsk }\n}\n"],"names":["useCallback","useEffect","useRef","useState","newAttemptKey","posDisplayCall","posDisplayTipCents","sale","pendingTipCents","pending","status","Math","max","round","tipCents","POS_DISPLAY_ANSWER_TIMEOUT_MS","usePosDisplay","user","hostId","registerId","connected","setConnected","asking","setAsking","cancelRef","userRef","current","uid","undefined","active","check","signedIn","result","action","Boolean","timer","setInterval","clearInterval","show","state","catch","cancelAsk","ask","promptId","slice","mode","Promise","resolve","done","finish","answer","clearTimeout","timeout","response","setTimeout"],"mappings":"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"}
@@ -1,13 +1,24 @@
1
1
  export interface PosDevicesCardProps {
2
2
  hostId: string;
3
+ /**
4
+ * Whether the viewer may work this site's register: `managePos`, as the
5
+ * shell resolved it, on a site role that may run it (admin or editor).
6
+ * Pairing a tablet is refused by the server otherwise, so the card says why
7
+ * rather than offering a button that fails. Absent reads as not allowed.
8
+ */
9
+ canManagePos?: boolean;
3
10
  }
4
11
  /**
5
12
  * The register's hardware (AGL-3607, AGL-3608): Stripe Terminal card readers
6
13
  * — registered with the code a reader shows, bound to a register, renamed,
7
14
  * removed — and the customer displays paired to each register, which can be
8
15
  * signed out. Readers appear only where card readers are offered at all.
16
+ *
17
+ * Each register pairs a new customer display or self-service kiosk here,
18
+ * through the same flow as the register's own Pair display (AGL-3712); a site
19
+ * with no register is sent to the card that makes one.
9
20
  */
10
- export declare function PosDevicesCard({ hostId }: PosDevicesCardProps): import("react").JSX.Element;
21
+ export declare function PosDevicesCard({ hostId, canManagePos }: PosDevicesCardProps): import("react").JSX.Element;
11
22
  export declare namespace PosDevicesCard {
12
23
  var displayName: string;
13
24
  }
@@ -15,15 +15,17 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import * as Aglyn from "@aglyn/aglyn";
19
20
  import { pluginDocsHelp } from "@aglyn/aglyn";
20
21
  import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
21
22
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
22
- import { useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
23
+ import { useFirestore, useFirestoreCollection, useOrgPlan, useUser } from "@aglyn/tenant-feature-instance";
23
24
  import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
24
25
  import { collection, limit, query } from "firebase/firestore";
25
26
  import { useCallback, useEffect, useState } from "react";
26
27
  import { posDisplayCall, posReadersCall, PosRequestError } from "./pos/pos-api.js";
28
+ import { POS_REGISTERS_ANCHOR, usePosPairing } from "./pos/pos-pairing.component.js";
27
29
  const EMPTY_ADDRESS = {
28
30
  line1: '',
29
31
  line2: '',
@@ -37,7 +39,11 @@ const EMPTY_ADDRESS = {
37
39
  * — registered with the code a reader shows, bound to a register, renamed,
38
40
  * removed — and the customer displays paired to each register, which can be
39
41
  * signed out. Readers appear only where card readers are offered at all.
40
- */ export function PosDevicesCard({ hostId }) {
42
+ *
43
+ * Each register pairs a new customer display or self-service kiosk here,
44
+ * through the same flow as the register's own Pair display (AGL-3712); a site
45
+ * with no register is sent to the card that makes one.
46
+ */ export function PosDevicesCard({ hostId, canManagePos = false }) {
41
47
  var _ref, _ref1, _ref2;
42
48
  const firestore = useFirestore();
43
49
  const { data: user } = useUser();
@@ -49,6 +55,8 @@ const EMPTY_ADDRESS = {
49
55
  ], {
50
56
  idField: '$id'
51
57
  });
58
+ // Not "no registers" until the listen has answered.
59
+ const noRegisters = registerDocs !== undefined && registerDocs.length === 0;
52
60
  const registers = [
53
61
  ...registerDocs != null ? registerDocs : []
54
62
  ].sort((a, b)=>{
@@ -64,12 +72,26 @@ const EMPTY_ADDRESS = {
64
72
  const [address, setAddress] = useState(EMPTY_ADDRESS);
65
73
  const [busy, setBusy] = useState(false);
66
74
  const [renaming, setRenaming] = useState(null);
75
+ const { org, ready: planReady } = useOrgPlan(hostId);
76
+ const posEntitled = planReady && Aglyn.checkEntitlement(org, 'pos');
67
77
  const notify = useCallback((error, fallback)=>enqueueSnackbar(error instanceof PosRequestError ? error.message : fallback, {
68
78
  variant: 'warning',
69
79
  persist: false
70
80
  }), [
71
81
  enqueueSnackbar
72
82
  ]);
83
+ const onPairingError = useCallback((message)=>enqueueSnackbar(message, {
84
+ variant: 'warning',
85
+ persist: false
86
+ }), [
87
+ enqueueSnackbar
88
+ ]);
89
+ const pairingFlow = usePosPairing({
90
+ user,
91
+ hostId,
92
+ onError: onPairingError
93
+ });
94
+ /** Why pairing is not offered to this viewer, or null when it is. */ const pairBlocked = !planReady ? 'Checking your plan…' : !posEntitled ? 'Point of Sale is not on your plan. Upgrade in Billing to pair displays and kiosks.' : !canManagePos ? 'Only an admin or editor with the "Use the point of sale" permission can pair displays and kiosks.' : null;
73
95
  const load = useCallback(async (refresh = false)=>{
74
96
  if (!user) return;
75
97
  try {
@@ -159,11 +181,22 @@ const EMPTY_ADDRESS = {
159
181
  help: pluginDocsHelp('pos', {
160
182
  anchor: '#card-readers'
161
183
  }),
162
- HeaderProps: showReaders ? {
163
- action: /*#__PURE__*/ _jsx(Button, {
164
- size: "small",
165
- onClick: ()=>setAdding(true),
166
- children: 'Add card reader'
184
+ HeaderProps: showReaders || noRegisters ? {
185
+ action: /*#__PURE__*/ _jsxs(Stack, {
186
+ direction: "row",
187
+ spacing: 1,
188
+ children: [
189
+ noRegisters ? /*#__PURE__*/ _jsx(Button, {
190
+ size: "small",
191
+ href: `#${POS_REGISTERS_ANCHOR}`,
192
+ children: 'Add a register'
193
+ }) : null,
194
+ showReaders ? /*#__PURE__*/ _jsx(Button, {
195
+ size: "small",
196
+ onClick: ()=>setAdding(true),
197
+ children: 'Add card reader'
198
+ }) : null
199
+ ]
167
200
  })
168
201
  } : undefined,
169
202
  contentGutterX: true,
@@ -294,58 +327,97 @@ const EMPTY_ADDRESS = {
294
327
  variant: "subtitle2",
295
328
  children: 'Customer displays and kiosks'
296
329
  }),
297
- /*#__PURE__*/ _jsx(Typography, {
330
+ noRegisters ? /*#__PURE__*/ _jsx(Typography, {
298
331
  variant: "body2",
299
332
  color: "text.secondary",
300
- children: 'Pair a tablet from the register: tap Pair display, choose Customer display or Self-service kiosk, and enter the code on the tablet.'
301
- }),
302
- registers.flatMap((register)=>{
303
- var _displays_register_$id;
304
- return ((_displays_register_$id = displays[register.$id]) != null ? _displays_register_$id : []).map((display)=>/*#__PURE__*/ _jsxs(Stack, {
305
- direction: "row",
306
- spacing: 1,
307
- sx: {
308
- alignItems: 'center'
309
- },
310
- children: [
311
- /*#__PURE__*/ _jsx(Typography, {
312
- variant: "body2",
313
- sx: {
314
- flex: 1
315
- },
316
- children: `${display.label} · ${register.name}`
317
- }),
318
- /*#__PURE__*/ _jsx(Chip, {
319
- size: "small",
320
- label: Date.now() - display.lastSeenAtMs < 120000 ? 'Connected' : 'Not connected',
321
- color: Date.now() - display.lastSeenAtMs < 120000 ? 'success' : 'default'
322
- }),
323
- /*#__PURE__*/ _jsx(Button, {
324
- size: "small",
325
- color: "error",
326
- onClick: async ()=>{
327
- if (!user) return;
328
- try {
329
- await posDisplayCall(user, {
330
- action: 'revoke',
331
- hostId,
332
- registerId: register.$id,
333
- displayId: display.id
334
- });
335
- setDisplays((prev)=>{
336
- var _prev_register_$id;
337
- return _extends({}, prev, {
338
- [register.$id]: ((_prev_register_$id = prev[register.$id]) != null ? _prev_register_$id : []).filter((entry)=>entry.id !== display.id)
339
- });
340
- });
341
- } catch (error) {
342
- notify(error, 'The display could not be signed out');
343
- }
344
- },
345
- children: 'Sign out'
346
- })
347
- ]
348
- }, `${register.$id}:${display.id}`));
333
+ children: 'A display or kiosk pairs to a register. Add a register first.'
334
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
335
+ children: [
336
+ /*#__PURE__*/ _jsx(Typography, {
337
+ variant: "body2",
338
+ color: "text.secondary",
339
+ children: pairBlocked != null ? pairBlocked : 'Pair display gives you a code. Open the address it shows on the tablet and enter the code.'
340
+ }),
341
+ registers.map((register)=>{
342
+ var _displays_register_$id;
343
+ return /*#__PURE__*/ _jsxs(Stack, {
344
+ spacing: 0.5,
345
+ children: [
346
+ /*#__PURE__*/ _jsxs(Stack, {
347
+ direction: "row",
348
+ spacing: 1,
349
+ sx: {
350
+ alignItems: 'center'
351
+ },
352
+ children: [
353
+ /*#__PURE__*/ _jsx(Typography, {
354
+ variant: "body2",
355
+ sx: {
356
+ flex: 1,
357
+ fontWeight: 'fontWeightMedium'
358
+ },
359
+ noWrap: true,
360
+ children: register.name
361
+ }),
362
+ /*#__PURE__*/ _jsx(Button, {
363
+ size: "small",
364
+ disabled: Boolean(pairBlocked),
365
+ onClick: ()=>void pairingFlow.open(register.$id, 'display', register.name),
366
+ "aria-label": `Pair display to ${register.name}`,
367
+ children: 'Pair display'
368
+ })
369
+ ]
370
+ }),
371
+ ((_displays_register_$id = displays[register.$id]) != null ? _displays_register_$id : []).map((display)=>/*#__PURE__*/ _jsxs(Stack, {
372
+ direction: "row",
373
+ spacing: 1,
374
+ sx: {
375
+ alignItems: 'center',
376
+ pl: 2
377
+ },
378
+ children: [
379
+ /*#__PURE__*/ _jsx(Typography, {
380
+ variant: "body2",
381
+ sx: {
382
+ flex: 1
383
+ },
384
+ children: display.label
385
+ }),
386
+ /*#__PURE__*/ _jsx(Chip, {
387
+ size: "small",
388
+ label: Date.now() - display.lastSeenAtMs < 120000 ? 'Connected' : 'Not connected',
389
+ color: Date.now() - display.lastSeenAtMs < 120000 ? 'success' : 'default'
390
+ }),
391
+ /*#__PURE__*/ _jsx(Button, {
392
+ size: "small",
393
+ color: "error",
394
+ onClick: async ()=>{
395
+ if (!user) return;
396
+ try {
397
+ await posDisplayCall(user, {
398
+ action: 'revoke',
399
+ hostId,
400
+ registerId: register.$id,
401
+ displayId: display.id
402
+ });
403
+ setDisplays((prev)=>{
404
+ var _prev_register_$id;
405
+ return _extends({}, prev, {
406
+ [register.$id]: ((_prev_register_$id = prev[register.$id]) != null ? _prev_register_$id : []).filter((entry)=>entry.id !== display.id)
407
+ });
408
+ });
409
+ } catch (error) {
410
+ notify(error, 'The display could not be signed out');
411
+ }
412
+ },
413
+ children: 'Sign out'
414
+ })
415
+ ]
416
+ }, `${register.$id}:${display.id}`))
417
+ ]
418
+ }, register.$id);
419
+ })
420
+ ]
349
421
  })
350
422
  ]
351
423
  })
@@ -536,7 +608,8 @@ const EMPTY_ADDRESS = {
536
608
  ]
537
609
  })
538
610
  ]
539
- })
611
+ }),
612
+ pairingFlow.dialog
540
613
  ]
541
614
  });
542
615
  }