@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.
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.d.ts +1 -1
- package/src/lib/components/commerce-console-page.js +139 -175
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/commerce-console-sections.d.ts +30 -2
- package/src/lib/components/commerce-console-sections.js +82 -7
- package/src/lib/components/commerce-console-sections.js.map +1 -1
- package/src/lib/components/console/discounts-card.component.js +36 -3
- package/src/lib/components/console/discounts-card.component.js.map +1 -1
- package/src/lib/components/console/host-coupons-card.component.js +1 -1
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-pairing.component.d.ts +59 -0
- package/src/lib/components/console/pos/pos-pairing.component.js +174 -0
- package/src/lib/components/console/pos/pos-pairing.component.js.map +1 -0
- package/src/lib/components/console/pos/use-pos-display.d.ts +0 -8
- package/src/lib/components/console/pos/use-pos-display.js +2 -16
- package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
- package/src/lib/components/console/pos-devices-card.component.d.ts +12 -1
- package/src/lib/components/console/pos-devices-card.component.js +132 -59
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.d.ts +1 -1
- package/src/lib/components/console/pos-page.component.js +37 -86
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/registers-card.component.js +13 -0
- package/src/lib/components/console/registers-card.component.js.map +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js +6 -11
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
- package/src/lib/components/console/store-zones.d.ts +15 -0
- package/src/lib/components/console/store-zones.js +36 -4
- package/src/lib/components/console/store-zones.js.map +1 -1
- package/src/lib/components/console/suppliers-card.component.js +2 -2
- package/src/lib/components/console/suppliers-card.component.js.map +1 -1
- package/src/lib/model/commerce-record-routes.js +10 -3
- package/src/lib/model/commerce-record-routes.js.map +1 -1
- package/src/lib/model/commerce-shipping.d.ts +16 -0
- package/src/lib/model/commerce-shipping.js +21 -0
- package/src/lib/model/commerce-shipping.js.map +1 -1
- package/src/lib/model/commerce-tax-decision.js +2 -2
- package/src/lib/model/commerce-tax-decision.js.map +1 -1
- package/src/lib/plugin.js +4 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +13 -0
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/order-webhooks.js +1 -1
- package/src/lib/server/order-webhooks.js.map +1 -1
- package/src/lib/server/shippingeasy.js +1 -1
- package/src/lib/server/shippingeasy.js.map +1 -1
- package/src/lib/server/store-setup-ai-capability.js +10 -2
- package/src/lib/server/store-setup-ai-capability.js.map +1 -1
- package/src/lib/server/supplier-routing.d.ts +86 -0
- package/src/lib/server/supplier-routing.js +175 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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__*/
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
children:
|
|
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: '
|
|
301
|
-
}),
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
}
|