@glitchgrab/whatsapp 0.1.0

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/dist/react.mjs ADDED
@@ -0,0 +1,435 @@
1
+ "use client";
2
+
3
+ // src/inbox.tsx
4
+ import { useMemo, useState as useState2 } from "react";
5
+
6
+ // src/use-inbox.ts
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ async function call(url, init) {
9
+ const res = await fetch(url, init);
10
+ const body = await res.json().catch(() => ({}));
11
+ if (!res.ok) throw new Error(body.error ?? `Request failed (${res.status})`);
12
+ return body;
13
+ }
14
+ function useInbox(options) {
15
+ const { api, live = true } = options;
16
+ const base = api.replace(/\/$/, "");
17
+ const [conversations, setConversations] = useState([]);
18
+ const [selected, setSelected] = useState(null);
19
+ const [agents, setAgents] = useState([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [sending, setSending] = useState(false);
22
+ const [error, setError] = useState(null);
23
+ const [connected, setConnected] = useState(false);
24
+ const selectedIdRef = useRef(options.initialConversationId ?? null);
25
+ const refresh = useCallback(async () => {
26
+ try {
27
+ const data = await call(`${base}/conversations`);
28
+ setConversations(data.conversations);
29
+ setError(null);
30
+ } catch (err) {
31
+ setError(err instanceof Error ? err.message : "Could not load conversations");
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }, [base]);
36
+ const openThread = useCallback(
37
+ async (conversationId) => {
38
+ selectedIdRef.current = conversationId;
39
+ try {
40
+ const data = await call(
41
+ `${base}/conversations/${conversationId}`
42
+ );
43
+ setSelected(data.conversation);
44
+ setConversations(
45
+ (prev) => prev.map((c2) => c2.id === conversationId ? { ...c2, unreadCount: 0 } : c2)
46
+ );
47
+ } catch (err) {
48
+ setError(err instanceof Error ? err.message : "Could not open conversation");
49
+ }
50
+ },
51
+ [base]
52
+ );
53
+ useEffect(() => {
54
+ void refresh();
55
+ void call(`${base}/agents`).then((d) => setAgents(d.agents)).catch(() => void 0);
56
+ if (options.initialConversationId) void openThread(options.initialConversationId);
57
+ }, [base, refresh, openThread, options.initialConversationId]);
58
+ useEffect(() => {
59
+ if (!live) return;
60
+ let source = null;
61
+ let cancelled = false;
62
+ let retry = null;
63
+ const connect = async () => {
64
+ if (cancelled) return;
65
+ try {
66
+ const session = await call(`${base}/ticket`, {
67
+ method: "POST"
68
+ });
69
+ if (cancelled) return;
70
+ source = new EventSource(
71
+ `${session.baseUrl}/api/v1/wa/inbox/stream?ticket=${encodeURIComponent(session.ticket)}`
72
+ );
73
+ source.addEventListener("ready", () => setConnected(true));
74
+ source.addEventListener("conversation", (event) => {
75
+ const updated = JSON.parse(event.data);
76
+ setConversations((prev) => {
77
+ const rest = prev.filter((c2) => c2.id !== updated.id);
78
+ return [updated, ...rest];
79
+ });
80
+ if (selectedIdRef.current === updated.id) void openThread(updated.id);
81
+ });
82
+ source.addEventListener("reconnect", () => {
83
+ source?.close();
84
+ setConnected(false);
85
+ retry = setTimeout(connect, 100);
86
+ });
87
+ source.onerror = () => {
88
+ setConnected(false);
89
+ source?.close();
90
+ retry = setTimeout(connect, 3e3);
91
+ };
92
+ } catch {
93
+ if (!cancelled) retry = setTimeout(connect, 5e3);
94
+ }
95
+ };
96
+ void connect();
97
+ return () => {
98
+ cancelled = true;
99
+ if (retry) clearTimeout(retry);
100
+ source?.close();
101
+ };
102
+ }, [base, live, openThread]);
103
+ const send = useCallback(
104
+ async (text) => {
105
+ if (!selected || !text.trim()) return;
106
+ setSending(true);
107
+ try {
108
+ await call(`${base}/send`, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ to: selected.contactPhone, body: text })
112
+ });
113
+ await openThread(selected.id);
114
+ setError(null);
115
+ } catch (err) {
116
+ setError(err instanceof Error ? err.message : "Could not send");
117
+ } finally {
118
+ setSending(false);
119
+ }
120
+ },
121
+ [base, selected, openThread]
122
+ );
123
+ const patch = useCallback(
124
+ async (body) => {
125
+ if (!selected) return;
126
+ try {
127
+ await call(`${base}/conversations/${selected.id}`, {
128
+ method: "PATCH",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify(body)
131
+ });
132
+ await openThread(selected.id);
133
+ } catch (err) {
134
+ setError(err instanceof Error ? err.message : "Could not update conversation");
135
+ }
136
+ },
137
+ [base, selected, openThread]
138
+ );
139
+ return {
140
+ conversations,
141
+ selected,
142
+ agents,
143
+ loading,
144
+ sending,
145
+ error,
146
+ connected,
147
+ select: (id) => void openThread(id),
148
+ send,
149
+ assign: (agentId) => patch({ assignedAgentId: agentId }),
150
+ setStatus: (status) => patch({ status }),
151
+ refresh
152
+ };
153
+ }
154
+
155
+ // src/inbox.tsx
156
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
157
+ var c = {
158
+ border: "var(--gg-wa-border, #e4e6eb)",
159
+ bg: "var(--gg-wa-bg, #ffffff)",
160
+ panel: "var(--gg-wa-panel, #f7f8fa)",
161
+ text: "var(--gg-wa-text, #111827)",
162
+ muted: "var(--gg-wa-muted, #6b7280)",
163
+ accent: "var(--gg-wa-accent, #128c7e)",
164
+ bubbleIn: "var(--gg-wa-bubble-in, #ffffff)",
165
+ bubbleOut: "var(--gg-wa-bubble-out, #d9fdd3)",
166
+ danger: "var(--gg-wa-danger, #b42318)"
167
+ };
168
+ function messageText(message) {
169
+ const payload = message.payload;
170
+ if (payload && "body" in payload && payload.body) return payload.body;
171
+ if (payload && "name" in payload && payload.name) return `\u{1F4C4} ${payload.name}`;
172
+ const inbound = payload?.messages?.[0];
173
+ return inbound?.text?.body ?? "(no text)";
174
+ }
175
+ function timeLabel(iso) {
176
+ const d = new Date(iso);
177
+ const today = /* @__PURE__ */ new Date();
178
+ const sameDay = d.toDateString() === today.toDateString();
179
+ return sameDay ? d.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString(void 0, { day: "numeric", month: "short" });
180
+ }
181
+ function ConversationRow(props) {
182
+ const { conversation: conv, active, onSelect } = props;
183
+ return /* @__PURE__ */ jsxs(
184
+ "button",
185
+ {
186
+ type: "button",
187
+ onClick: onSelect,
188
+ style: {
189
+ display: "block",
190
+ width: "100%",
191
+ textAlign: "left",
192
+ padding: "10px 12px",
193
+ border: "none",
194
+ borderBottom: `1px solid ${c.border}`,
195
+ background: active ? c.panel : "transparent",
196
+ cursor: "pointer",
197
+ font: "inherit",
198
+ color: c.text
199
+ },
200
+ children: [
201
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 8 }, children: [
202
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: conv.unreadCount > 0 ? 600 : 500, fontSize: 14 }, children: conv.contactName || conv.contactPhone }),
203
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: c.muted, whiteSpace: "nowrap" }, children: timeLabel(conv.updatedAt) })
204
+ ] }),
205
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 3 }, children: [
206
+ /* @__PURE__ */ jsx(
207
+ "span",
208
+ {
209
+ style: {
210
+ fontSize: 12,
211
+ color: c.muted,
212
+ overflow: "hidden",
213
+ textOverflow: "ellipsis",
214
+ whiteSpace: "nowrap",
215
+ flex: 1
216
+ },
217
+ children: conv.lastMessage ? messageText(conv.lastMessage) : "\u2014"
218
+ }
219
+ ),
220
+ conv.unreadCount > 0 && /* @__PURE__ */ jsx(
221
+ "span",
222
+ {
223
+ style: {
224
+ background: c.accent,
225
+ color: "#fff",
226
+ borderRadius: 999,
227
+ fontSize: 11,
228
+ padding: "1px 6px"
229
+ },
230
+ children: conv.unreadCount
231
+ }
232
+ ),
233
+ conv.optedOut && /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: c.danger, border: `1px solid ${c.danger}`, borderRadius: 4, padding: "0 4px" }, children: "opted out" })
234
+ ] })
235
+ ]
236
+ }
237
+ );
238
+ }
239
+ function WhatsappInbox(props) {
240
+ const { height = 600, emptyLabel = "No conversations yet", ...options } = props;
241
+ const inbox = useInbox(options);
242
+ const [draft, setDraft] = useState2("");
243
+ const selected = inbox.selected;
244
+ const windowOpen = selected?.windowOpen ?? false;
245
+ const orderedMessages = useMemo(() => selected?.messages ?? [], [selected]);
246
+ return /* @__PURE__ */ jsxs(
247
+ "div",
248
+ {
249
+ style: {
250
+ display: "flex",
251
+ height: typeof height === "number" ? `${height}px` : height,
252
+ border: `1px solid ${c.border}`,
253
+ borderRadius: 8,
254
+ overflow: "hidden",
255
+ background: c.bg,
256
+ color: c.text,
257
+ fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
258
+ },
259
+ children: [
260
+ /* @__PURE__ */ jsxs("aside", { style: { width: 280, borderRight: `1px solid ${c.border}`, display: "flex", flexDirection: "column" }, children: [
261
+ /* @__PURE__ */ jsxs(
262
+ "header",
263
+ {
264
+ style: {
265
+ padding: "10px 12px",
266
+ borderBottom: `1px solid ${c.border}`,
267
+ display: "flex",
268
+ alignItems: "center",
269
+ gap: 6
270
+ },
271
+ children: [
272
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 14, flex: 1 }, children: "Inbox" }),
273
+ /* @__PURE__ */ jsx(
274
+ "span",
275
+ {
276
+ title: inbox.connected ? "Live" : "Reconnecting\u2026",
277
+ style: {
278
+ width: 8,
279
+ height: 8,
280
+ borderRadius: 999,
281
+ background: inbox.connected ? c.accent : c.muted
282
+ }
283
+ }
284
+ )
285
+ ]
286
+ }
287
+ ),
288
+ /* @__PURE__ */ jsxs("div", { style: { overflowY: "auto", flex: 1 }, children: [
289
+ inbox.loading && /* @__PURE__ */ jsx("p", { style: { padding: 12, fontSize: 13, color: c.muted }, children: "Loading\u2026" }),
290
+ !inbox.loading && inbox.conversations.length === 0 && /* @__PURE__ */ jsx("p", { style: { padding: 12, fontSize: 13, color: c.muted }, children: emptyLabel }),
291
+ inbox.conversations.map((conv) => /* @__PURE__ */ jsx(
292
+ ConversationRow,
293
+ {
294
+ conversation: conv,
295
+ active: selected?.id === conv.id,
296
+ onSelect: () => inbox.select(conv.id)
297
+ },
298
+ conv.id
299
+ ))
300
+ ] })
301
+ ] }),
302
+ /* @__PURE__ */ jsxs("section", { style: { flex: 1, display: "flex", flexDirection: "column", background: c.panel }, children: [
303
+ !selected && /* @__PURE__ */ jsx("div", { style: { margin: "auto", color: c.muted, fontSize: 14 }, children: "Select a conversation" }),
304
+ selected && /* @__PURE__ */ jsxs(Fragment, { children: [
305
+ /* @__PURE__ */ jsxs(
306
+ "header",
307
+ {
308
+ style: {
309
+ padding: "10px 14px",
310
+ borderBottom: `1px solid ${c.border}`,
311
+ background: c.bg,
312
+ display: "flex",
313
+ alignItems: "center",
314
+ gap: 10
315
+ },
316
+ children: [
317
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
318
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 14, fontWeight: 600 }, children: selected.contactName || selected.contactPhone }),
319
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: c.muted }, children: selected.contactPhone })
320
+ ] }),
321
+ inbox.agents.length > 0 && /* @__PURE__ */ jsxs(
322
+ "select",
323
+ {
324
+ value: selected.assignedAgentId ?? "",
325
+ onChange: (e) => void inbox.assign(e.target.value || null),
326
+ style: { fontSize: 12, padding: "4px 6px", borderRadius: 6, border: `1px solid ${c.border}` },
327
+ children: [
328
+ /* @__PURE__ */ jsx("option", { value: "", children: "Unassigned" }),
329
+ inbox.agents.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
330
+ ]
331
+ }
332
+ ),
333
+ /* @__PURE__ */ jsxs(
334
+ "select",
335
+ {
336
+ value: selected.status,
337
+ onChange: (e) => void inbox.setStatus(e.target.value),
338
+ style: { fontSize: 12, padding: "4px 6px", borderRadius: 6, border: `1px solid ${c.border}` },
339
+ children: [
340
+ /* @__PURE__ */ jsx("option", { value: "OPEN", children: "Open" }),
341
+ /* @__PURE__ */ jsx("option", { value: "SNOOZED", children: "Snoozed" }),
342
+ /* @__PURE__ */ jsx("option", { value: "CLOSED", children: "Closed" })
343
+ ]
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto", padding: 14, display: "flex", flexDirection: "column", gap: 8 }, children: orderedMessages.map((m) => {
350
+ const outbound = m.direction === "OUTBOUND";
351
+ return /* @__PURE__ */ jsxs(
352
+ "div",
353
+ {
354
+ style: {
355
+ alignSelf: outbound ? "flex-end" : "flex-start",
356
+ maxWidth: "72%",
357
+ background: outbound ? c.bubbleOut : c.bubbleIn,
358
+ border: `1px solid ${c.border}`,
359
+ borderRadius: 10,
360
+ padding: "7px 10px"
361
+ },
362
+ children: [
363
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: messageText(m) }),
364
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 10, color: m.status === "FAILED" ? c.danger : c.muted, marginTop: 3, textAlign: "right" }, children: [
365
+ timeLabel(m.createdAt),
366
+ outbound && ` \xB7 ${m.status.toLowerCase()}`
367
+ ] })
368
+ ]
369
+ },
370
+ m.id
371
+ );
372
+ }) }),
373
+ /* @__PURE__ */ jsxs("footer", { style: { borderTop: `1px solid ${c.border}`, background: c.bg, padding: 10 }, children: [
374
+ inbox.error && /* @__PURE__ */ jsx("p", { style: { margin: "0 0 8px", fontSize: 12, color: c.danger }, children: inbox.error }),
375
+ !windowOpen ? /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 12, color: c.muted }, children: "This contact hasn't messaged in the last 24 hours, so a free reply can't be delivered. Send an approved template instead." }) : /* @__PURE__ */ jsxs(
376
+ "form",
377
+ {
378
+ onSubmit: (e) => {
379
+ e.preventDefault();
380
+ const text = draft;
381
+ setDraft("");
382
+ void inbox.send(text);
383
+ },
384
+ style: { display: "flex", gap: 8 },
385
+ children: [
386
+ /* @__PURE__ */ jsx(
387
+ "input",
388
+ {
389
+ value: draft,
390
+ onChange: (e) => setDraft(e.target.value),
391
+ placeholder: "Type a reply\u2026",
392
+ disabled: inbox.sending,
393
+ style: {
394
+ flex: 1,
395
+ padding: "8px 10px",
396
+ borderRadius: 8,
397
+ border: `1px solid ${c.border}`,
398
+ font: "inherit",
399
+ fontSize: 13
400
+ }
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsx(
404
+ "button",
405
+ {
406
+ type: "submit",
407
+ disabled: inbox.sending || !draft.trim(),
408
+ style: {
409
+ padding: "8px 14px",
410
+ borderRadius: 8,
411
+ border: "none",
412
+ background: c.accent,
413
+ color: "#fff",
414
+ fontSize: 13,
415
+ cursor: inbox.sending || !draft.trim() ? "not-allowed" : "pointer",
416
+ opacity: inbox.sending || !draft.trim() ? 0.6 : 1
417
+ },
418
+ children: inbox.sending ? "Sending\u2026" : "Send"
419
+ }
420
+ )
421
+ ]
422
+ }
423
+ )
424
+ ] })
425
+ ] })
426
+ ] })
427
+ ]
428
+ }
429
+ );
430
+ }
431
+ export {
432
+ WhatsappInbox,
433
+ useInbox
434
+ };
435
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inbox.tsx","../src/use-inbox.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { useInbox, type UseInboxOptions } from \"./use-inbox\";\nimport type { WaConversation, WaMessage } from \"./types\";\n\n/**\n * A drop-in shared inbox.\n *\n * Styled with inline styles rather than a CSS file or a framework: this ships\n * into other people's apps, and a stylesheet that has to be imported (or a\n * Tailwind class that only works if they use Tailwind) is the fastest way to\n * make a \"drop-in\" component not drop in. Colours come from CSS custom\n * properties so a host can restyle without forking.\n */\n\nexport interface WhatsappInboxProps extends UseInboxOptions {\n height?: number | string;\n emptyLabel?: string;\n}\n\nconst c = {\n border: \"var(--gg-wa-border, #e4e6eb)\",\n bg: \"var(--gg-wa-bg, #ffffff)\",\n panel: \"var(--gg-wa-panel, #f7f8fa)\",\n text: \"var(--gg-wa-text, #111827)\",\n muted: \"var(--gg-wa-muted, #6b7280)\",\n accent: \"var(--gg-wa-accent, #128c7e)\",\n bubbleIn: \"var(--gg-wa-bubble-in, #ffffff)\",\n bubbleOut: \"var(--gg-wa-bubble-out, #d9fdd3)\",\n danger: \"var(--gg-wa-danger, #b42318)\",\n};\n\nfunction messageText(message: WaMessage): string {\n const payload = message.payload as\n | { type?: string; body?: string; name?: string }\n | { messages?: { text?: { body?: string } }[] }\n | undefined;\n\n if (payload && \"body\" in payload && payload.body) return payload.body;\n if (payload && \"name\" in payload && payload.name) return `📄 ${payload.name}`;\n\n // Inbound rows store Meta's raw webhook value, which nests the text.\n const inbound = (payload as { messages?: { text?: { body?: string } }[] } | undefined)?.messages?.[0];\n return inbound?.text?.body ?? \"(no text)\";\n}\n\nfunction timeLabel(iso: string): string {\n const d = new Date(iso);\n const today = new Date();\n const sameDay = d.toDateString() === today.toDateString();\n return sameDay\n ? d.toLocaleTimeString(undefined, { hour: \"2-digit\", minute: \"2-digit\" })\n : d.toLocaleDateString(undefined, { day: \"numeric\", month: \"short\" });\n}\n\nfunction ConversationRow(props: {\n conversation: WaConversation;\n active: boolean;\n onSelect: () => void;\n}) {\n const { conversation: conv, active, onSelect } = props;\n return (\n <button\n type=\"button\"\n onClick={onSelect}\n style={{\n display: \"block\",\n width: \"100%\",\n textAlign: \"left\",\n padding: \"10px 12px\",\n border: \"none\",\n borderBottom: `1px solid ${c.border}`,\n background: active ? c.panel : \"transparent\",\n cursor: \"pointer\",\n font: \"inherit\",\n color: c.text,\n }}\n >\n <div style={{ display: \"flex\", justifyContent: \"space-between\", gap: 8 }}>\n <span style={{ fontWeight: conv.unreadCount > 0 ? 600 : 500, fontSize: 14 }}>\n {conv.contactName || conv.contactPhone}\n </span>\n <span style={{ fontSize: 11, color: c.muted, whiteSpace: \"nowrap\" }}>\n {timeLabel(conv.updatedAt)}\n </span>\n </div>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, marginTop: 3 }}>\n <span\n style={{\n fontSize: 12,\n color: c.muted,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n flex: 1,\n }}\n >\n {conv.lastMessage ? messageText(conv.lastMessage) : \"—\"}\n </span>\n {conv.unreadCount > 0 && (\n <span\n style={{\n background: c.accent,\n color: \"#fff\",\n borderRadius: 999,\n fontSize: 11,\n padding: \"1px 6px\",\n }}\n >\n {conv.unreadCount}\n </span>\n )}\n {/* Opted out is worth a permanent marker: it silently changes what an\n agent is allowed to send, and there is no error until they try. */}\n {conv.optedOut && (\n <span style={{ fontSize: 10, color: c.danger, border: `1px solid ${c.danger}`, borderRadius: 4, padding: \"0 4px\" }}>\n opted out\n </span>\n )}\n </div>\n </button>\n );\n}\n\nexport function WhatsappInbox(props: WhatsappInboxProps) {\n const { height = 600, emptyLabel = \"No conversations yet\", ...options } = props;\n const inbox = useInbox(options);\n const [draft, setDraft] = useState(\"\");\n\n const selected = inbox.selected;\n const windowOpen = selected?.windowOpen ?? false;\n\n const orderedMessages = useMemo(() => selected?.messages ?? [], [selected]);\n\n return (\n <div\n style={{\n display: \"flex\",\n height: typeof height === \"number\" ? `${height}px` : height,\n border: `1px solid ${c.border}`,\n borderRadius: 8,\n overflow: \"hidden\",\n background: c.bg,\n color: c.text,\n fontFamily:\n \"system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif\",\n }}\n >\n <aside style={{ width: 280, borderRight: `1px solid ${c.border}`, display: \"flex\", flexDirection: \"column\" }}>\n <header\n style={{\n padding: \"10px 12px\",\n borderBottom: `1px solid ${c.border}`,\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n }}\n >\n <strong style={{ fontSize: 14, flex: 1 }}>Inbox</strong>\n {/* A dot rather than a label: agents need to know the feed is live,\n not read a status sentence on every render. */}\n <span\n title={inbox.connected ? \"Live\" : \"Reconnecting…\"}\n style={{\n width: 8,\n height: 8,\n borderRadius: 999,\n background: inbox.connected ? c.accent : c.muted,\n }}\n />\n </header>\n\n <div style={{ overflowY: \"auto\", flex: 1 }}>\n {inbox.loading && <p style={{ padding: 12, fontSize: 13, color: c.muted }}>Loading…</p>}\n {!inbox.loading && inbox.conversations.length === 0 && (\n <p style={{ padding: 12, fontSize: 13, color: c.muted }}>{emptyLabel}</p>\n )}\n {inbox.conversations.map((conv) => (\n <ConversationRow\n key={conv.id}\n conversation={conv}\n active={selected?.id === conv.id}\n onSelect={() => inbox.select(conv.id)}\n />\n ))}\n </div>\n </aside>\n\n <section style={{ flex: 1, display: \"flex\", flexDirection: \"column\", background: c.panel }}>\n {!selected && (\n <div style={{ margin: \"auto\", color: c.muted, fontSize: 14 }}>\n Select a conversation\n </div>\n )}\n\n {selected && (\n <>\n <header\n style={{\n padding: \"10px 14px\",\n borderBottom: `1px solid ${c.border}`,\n background: c.bg,\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n }}\n >\n <div style={{ flex: 1 }}>\n <div style={{ fontSize: 14, fontWeight: 600 }}>\n {selected.contactName || selected.contactPhone}\n </div>\n <div style={{ fontSize: 11, color: c.muted }}>{selected.contactPhone}</div>\n </div>\n\n {inbox.agents.length > 0 && (\n <select\n value={selected.assignedAgentId ?? \"\"}\n onChange={(e) => void inbox.assign(e.target.value || null)}\n style={{ fontSize: 12, padding: \"4px 6px\", borderRadius: 6, border: `1px solid ${c.border}` }}\n >\n <option value=\"\">Unassigned</option>\n {inbox.agents.map((a) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n )}\n\n <select\n value={selected.status}\n onChange={(e) => void inbox.setStatus(e.target.value as \"OPEN\" | \"SNOOZED\" | \"CLOSED\")}\n style={{ fontSize: 12, padding: \"4px 6px\", borderRadius: 6, border: `1px solid ${c.border}` }}\n >\n <option value=\"OPEN\">Open</option>\n <option value=\"SNOOZED\">Snoozed</option>\n <option value=\"CLOSED\">Closed</option>\n </select>\n </header>\n\n <div style={{ flex: 1, overflowY: \"auto\", padding: 14, display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n {orderedMessages.map((m) => {\n const outbound = m.direction === \"OUTBOUND\";\n return (\n <div\n key={m.id}\n style={{\n alignSelf: outbound ? \"flex-end\" : \"flex-start\",\n maxWidth: \"72%\",\n background: outbound ? c.bubbleOut : c.bubbleIn,\n border: `1px solid ${c.border}`,\n borderRadius: 10,\n padding: \"7px 10px\",\n }}\n >\n <div style={{ fontSize: 13, whiteSpace: \"pre-wrap\", wordBreak: \"break-word\" }}>\n {messageText(m)}\n </div>\n <div style={{ fontSize: 10, color: m.status === \"FAILED\" ? c.danger : c.muted, marginTop: 3, textAlign: \"right\" }}>\n {timeLabel(m.createdAt)}\n {outbound && ` · ${m.status.toLowerCase()}`}\n </div>\n </div>\n );\n })}\n </div>\n\n <footer style={{ borderTop: `1px solid ${c.border}`, background: c.bg, padding: 10 }}>\n {inbox.error && (\n <p style={{ margin: \"0 0 8px\", fontSize: 12, color: c.danger }}>{inbox.error}</p>\n )}\n\n {/* The single most confusing thing about WhatsApp for an agent:\n outside the 24-hour window free text is accepted by Meta and\n never delivered. Saying so up front beats a silent failure. */}\n {!windowOpen ? (\n <p style={{ margin: 0, fontSize: 12, color: c.muted }}>\n This contact hasn&apos;t messaged in the last 24 hours, so a free reply\n can&apos;t be delivered. Send an approved template instead.\n </p>\n ) : (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const text = draft;\n setDraft(\"\");\n void inbox.send(text);\n }}\n style={{ display: \"flex\", gap: 8 }}\n >\n <input\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Type a reply…\"\n disabled={inbox.sending}\n style={{\n flex: 1,\n padding: \"8px 10px\",\n borderRadius: 8,\n border: `1px solid ${c.border}`,\n font: \"inherit\",\n fontSize: 13,\n }}\n />\n <button\n type=\"submit\"\n disabled={inbox.sending || !draft.trim()}\n style={{\n padding: \"8px 14px\",\n borderRadius: 8,\n border: \"none\",\n background: c.accent,\n color: \"#fff\",\n fontSize: 13,\n cursor: inbox.sending || !draft.trim() ? \"not-allowed\" : \"pointer\",\n opacity: inbox.sending || !draft.trim() ? 0.6 : 1,\n }}\n >\n {inbox.sending ? \"Sending…\" : \"Send\"}\n </button>\n </form>\n )}\n </footer>\n </>\n )}\n </section>\n </div>\n );\n}\n","\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { WaAgent, WaConversation, WaMessage } from \"./types\";\n\n/**\n * The inbox's data layer.\n *\n * Talks to *your* proxy route (see `createInboxHandler`), never to us directly —\n * except the SSE stream, which needs a long-lived connection and so goes to us\n * with a short-lived ticket your route mints.\n */\n\nexport interface UseInboxOptions {\n /** Where you mounted `createInboxHandler`. */\n api: string;\n /** Open this thread on mount. */\n initialConversationId?: string;\n /** Set false to load once and skip the live stream. */\n live?: boolean;\n}\n\nexport interface UseInboxResult {\n conversations: WaConversation[];\n selected: (WaConversation & { messages: WaMessage[] }) | null;\n agents: WaAgent[];\n loading: boolean;\n sending: boolean;\n error: string | null;\n connected: boolean;\n select: (conversationId: string) => void;\n send: (text: string) => Promise<void>;\n assign: (agentId: string | null) => Promise<void>;\n setStatus: (status: \"OPEN\" | \"SNOOZED\" | \"CLOSED\") => Promise<void>;\n refresh: () => Promise<void>;\n}\n\nasync function call<T>(url: string, init?: RequestInit): Promise<T> {\n const res = await fetch(url, init);\n const body = (await res.json().catch(() => ({}))) as T & { error?: string };\n if (!res.ok) throw new Error(body.error ?? `Request failed (${res.status})`);\n return body as T;\n}\n\nexport function useInbox(options: UseInboxOptions): UseInboxResult {\n const { api, live = true } = options;\n const base = api.replace(/\\/$/, \"\");\n\n const [conversations, setConversations] = useState<WaConversation[]>([]);\n const [selected, setSelected] = useState<(WaConversation & { messages: WaMessage[] }) | null>(null);\n const [agents, setAgents] = useState<WaAgent[]>([]);\n const [loading, setLoading] = useState(true);\n const [sending, setSending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [connected, setConnected] = useState(false);\n\n const selectedIdRef = useRef<string | null>(options.initialConversationId ?? null);\n\n const refresh = useCallback(async () => {\n try {\n const data = await call<{ conversations: WaConversation[] }>(`${base}/conversations`);\n setConversations(data.conversations);\n setError(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Could not load conversations\");\n } finally {\n setLoading(false);\n }\n }, [base]);\n\n const openThread = useCallback(\n async (conversationId: string) => {\n selectedIdRef.current = conversationId;\n try {\n const data = await call<{ conversation: WaConversation & { messages: WaMessage[] } }>(\n `${base}/conversations/${conversationId}`\n );\n setSelected(data.conversation);\n // Reading clears the badge server-side; mirror it so the list does not\n // keep showing an unread count until the next stream tick.\n setConversations((prev) =>\n prev.map((c) => (c.id === conversationId ? { ...c, unreadCount: 0 } : c))\n );\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Could not open conversation\");\n }\n },\n [base]\n );\n\n useEffect(() => {\n void refresh();\n void call<{ agents: WaAgent[] }>(`${base}/agents`)\n .then((d) => setAgents(d.agents))\n .catch(() => undefined); // seats are optional; the inbox works without them\n if (options.initialConversationId) void openThread(options.initialConversationId);\n }, [base, refresh, openThread, options.initialConversationId]);\n\n /**\n * The live stream.\n *\n * `EventSource` reconnects on its own and replays `Last-Event-ID`, so a\n * dropped connection resumes rather than losing messages. The server closes\n * the stream deliberately before the platform's function timeout; that arrives\n * as a `reconnect` event and is a normal part of the lifecycle, not an error.\n */\n useEffect(() => {\n if (!live) return;\n\n let source: EventSource | null = null;\n let cancelled = false;\n let retry: ReturnType<typeof setTimeout> | null = null;\n\n const connect = async () => {\n if (cancelled) return;\n try {\n const session = await call<{ ticket: string; baseUrl: string }>(`${base}/ticket`, {\n method: \"POST\",\n });\n if (cancelled) return;\n\n source = new EventSource(\n `${session.baseUrl}/api/v1/wa/inbox/stream?ticket=${encodeURIComponent(session.ticket)}`\n );\n\n source.addEventListener(\"ready\", () => setConnected(true));\n\n source.addEventListener(\"conversation\", (event) => {\n const updated = JSON.parse((event as MessageEvent).data) as WaConversation;\n setConversations((prev) => {\n const rest = prev.filter((c) => c.id !== updated.id);\n return [updated, ...rest];\n });\n // Refresh the open thread so a new message appears in it, not just in\n // the list beside it.\n if (selectedIdRef.current === updated.id) void openThread(updated.id);\n });\n\n source.addEventListener(\"reconnect\", () => {\n source?.close();\n setConnected(false);\n // Immediate: the server closed on purpose and a new ticket is needed.\n retry = setTimeout(connect, 100);\n });\n\n source.onerror = () => {\n setConnected(false);\n source?.close();\n // The ticket may simply have expired, so reconnect rather than\n // surfacing an error the user cannot act on.\n retry = setTimeout(connect, 3000);\n };\n } catch {\n if (!cancelled) retry = setTimeout(connect, 5000);\n }\n };\n\n void connect();\n\n return () => {\n cancelled = true;\n if (retry) clearTimeout(retry);\n source?.close();\n };\n }, [base, live, openThread]);\n\n const send = useCallback(\n async (text: string) => {\n if (!selected || !text.trim()) return;\n setSending(true);\n try {\n await call(`${base}/send`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ to: selected.contactPhone, body: text }),\n });\n await openThread(selected.id);\n setError(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Could not send\");\n } finally {\n setSending(false);\n }\n },\n [base, selected, openThread]\n );\n\n const patch = useCallback(\n async (body: Record<string, unknown>) => {\n if (!selected) return;\n try {\n await call(`${base}/conversations/${selected.id}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n await openThread(selected.id);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Could not update conversation\");\n }\n },\n [base, selected, openThread]\n );\n\n return {\n conversations,\n selected,\n agents,\n loading,\n sending,\n error,\n connected,\n select: (id) => void openThread(id),\n send,\n assign: (agentId) => patch({ assignedAgentId: agentId }),\n setStatus: (status) => patch({ status }),\n refresh,\n };\n}\n"],"mappings":"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","names":["useState","c","useState"]}
@@ -0,0 +1,97 @@
1
+ /** Shared shapes. Mirrors the `/api/v1/wa` responses. */
2
+ type WaTemplateCategory = "MARKETING" | "UTILITY" | "AUTHENTICATION" | "SERVICE";
3
+ type WaTemplateStatus = "DRAFT" | "PENDING" | "APPROVED" | "REJECTED" | "PAUSED" | "DISABLED";
4
+ type WaMessageDirection = "INBOUND" | "OUTBOUND";
5
+ type WaMessageStatus = "QUEUED" | "SENT" | "DELIVERED" | "READ" | "FAILED";
6
+ type WaConversationStatus = "OPEN" | "SNOOZED" | "CLOSED";
7
+ type WaMatchType = "EXACT" | "CONTAINS" | "STARTS_WITH" | "REGEX" | "ANY";
8
+ /**
9
+ * Every failure carries a stable `code`, so callers branch on that rather than
10
+ * matching on a message that may be reworded.
11
+ */
12
+ type WaErrorCode = "UNAUTHORIZED" | "PLATFORM_INACTIVE" | "TENANT_NOT_FOUND" | "INSUFFICIENT_FUNDS" | "NO_PRICE_RULE" | "INVALID_AMOUNT" | "DUPLICATE_REQUEST" | "INTERNAL";
13
+ declare class WhatsappError extends Error {
14
+ readonly code: WaErrorCode | string;
15
+ readonly status: number;
16
+ readonly detail?: Record<string, unknown> | undefined;
17
+ constructor(code: WaErrorCode | string, message: string, status: number, detail?: Record<string, unknown> | undefined);
18
+ /** The owner has run out of balance. `detail.shortfallPaise` says by how much. */
19
+ get isInsufficientFunds(): boolean;
20
+ }
21
+ interface WaNumber {
22
+ phoneNumberId: string;
23
+ displayNumber: string;
24
+ verifiedName: string;
25
+ status: "PENDING" | "VERIFIED" | "REGISTERED" | "DISCONNECTED";
26
+ qualityRating?: string | null;
27
+ messagingLimitTier?: string | null;
28
+ }
29
+ interface WaMessage {
30
+ id: string;
31
+ direction: WaMessageDirection;
32
+ status: WaMessageStatus;
33
+ contactPhone?: string;
34
+ category?: WaTemplateCategory | null;
35
+ payload: unknown;
36
+ error?: string | null;
37
+ sentAt?: string | null;
38
+ deliveredAt?: string | null;
39
+ readAt?: string | null;
40
+ createdAt: string;
41
+ }
42
+ interface WaConversation {
43
+ id: string;
44
+ contactPhone: string;
45
+ contactName?: string | null;
46
+ windowExpiresAt?: string | null;
47
+ /** Whether free-form text is legal right now. Computed server-side. */
48
+ windowOpen: boolean;
49
+ lastInboundAt?: string | null;
50
+ lastOutboundAt?: string | null;
51
+ optedOut: boolean;
52
+ status: WaConversationStatus;
53
+ assignedAgentId?: string | null;
54
+ unreadCount: number;
55
+ updatedAt: string;
56
+ lastMessage?: WaMessage | null;
57
+ }
58
+ interface WaTemplate {
59
+ id: string;
60
+ name: string;
61
+ language: string;
62
+ category: WaTemplateCategory;
63
+ status: WaTemplateStatus;
64
+ rejectionReason?: string | null;
65
+ components?: unknown;
66
+ submittedAt?: string | null;
67
+ lastSyncedAt?: string | null;
68
+ }
69
+ interface WaAgent {
70
+ id: string;
71
+ externalAgentId: string;
72
+ name: string;
73
+ email?: string | null;
74
+ role: "AGENT" | "ADMIN";
75
+ active: boolean;
76
+ }
77
+ interface WaBalance {
78
+ balancePaise: number;
79
+ heldPaise: number;
80
+ spendablePaise: number;
81
+ }
82
+ interface WaSendResult {
83
+ messageId: string;
84
+ metaMessageId: string;
85
+ status: "SENT";
86
+ category: WaTemplateCategory;
87
+ tenantPricePaise: number;
88
+ tenantBalancePaise: number;
89
+ }
90
+ interface WaSignupLaunch {
91
+ appId: string;
92
+ configId: string;
93
+ scopes: string[];
94
+ state: string;
95
+ }
96
+
97
+ export { type WaAgent as W, type WaBalance as a, type WaConversation as b, type WaConversationStatus as c, type WaErrorCode as d, type WaMatchType as e, type WaMessage as f, type WaMessageDirection as g, type WaMessageStatus as h, type WaNumber as i, type WaSendResult as j, type WaSignupLaunch as k, type WaTemplate as l, type WaTemplateCategory as m, type WaTemplateStatus as n, WhatsappError as o };