@ezychat/web 0.1.55 → 0.1.57

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.
@@ -92,7 +92,7 @@ export declare function useAiTry(): import("@tanstack/react-query").UseMutationR
92
92
  action: "answer" | "ask_resolution" | "resolve" | "handoff" | "no_reply" | null;
93
93
  model: string | null;
94
94
  error: string | null;
95
- handoffReason?: "asked_for_human" | "missing_facts" | "sensitive" | "needs_action" | "business_rule" | "unsupported_message" | "ai_unavailable" | "lookup_failed" | "confidential_leak" | null | undefined;
95
+ handoffReason?: "asked_for_human" | "missing_facts" | "sensitive" | "needs_action" | "business_rule" | "unsupported_message" | "ai_unavailable" | "lookup_failed" | "confidential_leak" | "window_closed" | null | undefined;
96
96
  lookups?: {
97
97
  title: string;
98
98
  sourceLabel: string;
@@ -17,6 +17,15 @@ export declare function useCreateInbox(): import("@tanstack/react-query").UseMut
17
17
  };
18
18
  removed: boolean;
19
19
  aiAgentId: number | null;
20
+ type: string;
21
+ capabilities: {
22
+ qrLink: boolean;
23
+ history: boolean;
24
+ groups: boolean;
25
+ replyWindowHours: number | null;
26
+ templates: boolean;
27
+ needsPublicUrl: boolean;
28
+ };
20
29
  memberIds?: number[] | undefined;
21
30
  }, Error, {
22
31
  name: string;
@@ -40,6 +49,15 @@ export declare function useUpdateInbox(): import("@tanstack/react-query").UseMut
40
49
  };
41
50
  removed: boolean;
42
51
  aiAgentId: number | null;
52
+ type: string;
53
+ capabilities: {
54
+ qrLink: boolean;
55
+ history: boolean;
56
+ groups: boolean;
57
+ replyWindowHours: number | null;
58
+ templates: boolean;
59
+ needsPublicUrl: boolean;
60
+ };
43
61
  memberIds?: number[] | undefined;
44
62
  }, Error, {
45
63
  id: number;
@@ -140,6 +140,7 @@ export declare function useChats(filters: ChatFilters): import("@tanstack/react-
140
140
  assignedTo: number | null;
141
141
  updatedAt: number;
142
142
  phone: string | null;
143
+ lastInboundAt: number | null;
143
144
  tags?: string[] | undefined;
144
145
  whatsappName?: string | null | undefined;
145
146
  }[];
@@ -160,6 +161,7 @@ export declare function useChat(jid: string | null | undefined): import("@tansta
160
161
  assignedTo: number | null;
161
162
  updatedAt: number;
162
163
  phone: string | null;
164
+ lastInboundAt: number | null;
163
165
  tags?: string[] | undefined;
164
166
  whatsappName?: string | null | undefined;
165
167
  };
@@ -207,6 +209,10 @@ export declare function useMessages(jid: string | null | undefined): import("@ta
207
209
  senderName?: string | null | undefined;
208
210
  }[];
209
211
  voiceNote?: boolean | undefined;
212
+ template?: {
213
+ name: string;
214
+ language: string;
215
+ } | null | undefined;
210
216
  viaAssistant?: boolean | undefined;
211
217
  transcript?: string | null | undefined;
212
218
  transcriptLang?: string | null | undefined;
@@ -254,6 +260,10 @@ export declare function useSendText(jid: string): import("@tanstack/react-query"
254
260
  senderName?: string | null | undefined;
255
261
  }[];
256
262
  voiceNote?: boolean | undefined;
263
+ template?: {
264
+ name: string;
265
+ language: string;
266
+ } | null | undefined;
257
267
  viaAssistant?: boolean | undefined;
258
268
  transcript?: string | null | undefined;
259
269
  transcriptLang?: string | null | undefined;
@@ -301,6 +311,10 @@ export declare function useSendMedia(jid: string): import("@tanstack/react-query
301
311
  senderName?: string | null | undefined;
302
312
  }[];
303
313
  voiceNote?: boolean | undefined;
314
+ template?: {
315
+ name: string;
316
+ language: string;
317
+ } | null | undefined;
304
318
  viaAssistant?: boolean | undefined;
305
319
  transcript?: string | null | undefined;
306
320
  transcriptLang?: string | null | undefined;
@@ -337,6 +351,10 @@ export declare function useRetryMessage(): import("@tanstack/react-query").UseMu
337
351
  senderName?: string | null | undefined;
338
352
  }[];
339
353
  voiceNote?: boolean | undefined;
354
+ template?: {
355
+ name: string;
356
+ language: string;
357
+ } | null | undefined;
340
358
  viaAssistant?: boolean | undefined;
341
359
  transcript?: string | null | undefined;
342
360
  transcriptLang?: string | null | undefined;
@@ -379,6 +397,7 @@ export declare function usePatchChat(jid: string): import("@tanstack/react-query
379
397
  assignedTo: number | null;
380
398
  updatedAt: number;
381
399
  phone: string | null;
400
+ lastInboundAt: number | null;
382
401
  tags?: string[] | undefined;
383
402
  whatsappName?: string | null | undefined;
384
403
  }, Error, {
@@ -621,6 +640,15 @@ export declare function useInboxes(opts?: {
621
640
  };
622
641
  removed: boolean;
623
642
  aiAgentId: number | null;
643
+ type: string;
644
+ capabilities: {
645
+ qrLink: boolean;
646
+ history: boolean;
647
+ groups: boolean;
648
+ replyWindowHours: number | null;
649
+ templates: boolean;
650
+ needsPublicUrl: boolean;
651
+ };
624
652
  memberIds?: number[] | undefined;
625
653
  }[];
626
654
  maxInboxes: number;
@@ -0,0 +1,75 @@
1
+ export declare const templatesKey: (inboxId: number) => readonly ["templates", number];
2
+ /** Approved message templates of a Business API inbox (`GET /api/inboxes/:id/templates`). */
3
+ export declare function useTemplates(inboxId: number, enabled: boolean): import("@tanstack/react-query").UseQueryResult<{
4
+ templates: {
5
+ name: string;
6
+ language: string;
7
+ category: "utility" | "marketing" | "authentication";
8
+ body: string;
9
+ variables: number;
10
+ examples: string[];
11
+ header: "text" | "image" | "video" | "document" | null;
12
+ buttons: boolean;
13
+ sendable: boolean;
14
+ }[];
15
+ }, Error>;
16
+ /** Sync: reads the templates again from WhatsApp (`?refresh=1`) and replaces the cached list. */
17
+ export declare function useSyncTemplates(inboxId: number): import("@tanstack/react-query").UseMutationResult<{
18
+ templates: {
19
+ name: string;
20
+ language: string;
21
+ category: "utility" | "marketing" | "authentication";
22
+ body: string;
23
+ variables: number;
24
+ examples: string[];
25
+ header: "text" | "image" | "video" | "document" | null;
26
+ buttons: boolean;
27
+ sendable: boolean;
28
+ }[];
29
+ }, Error, void, unknown>;
30
+ /** Sends an approved template into a chat (also after its reply window closed). */
31
+ export declare function useSendTemplate(jid: string): import("@tanstack/react-query").UseMutationResult<{
32
+ id: string;
33
+ chatJid: string;
34
+ senderJid: string | null;
35
+ senderName: string | null;
36
+ fromMe: boolean;
37
+ sentByUserId: number | null;
38
+ type: "text" | "image" | "video" | "audio" | "document" | "sticker" | "system";
39
+ body: string | null;
40
+ mediaUrl: string | null;
41
+ mediaMime: string | null;
42
+ mediaName: string | null;
43
+ mediaStatus: "none" | "pending" | "failed" | "ok";
44
+ quotedId: string | null;
45
+ status: "pending" | "failed" | "sent" | "delivered" | "read";
46
+ error: string | null;
47
+ timestamp: number;
48
+ clientId: string | null;
49
+ reactions: {
50
+ emoji: string;
51
+ senderJid: string | null;
52
+ fromMe: boolean;
53
+ at: number;
54
+ sentByUserId: number | null;
55
+ senderName?: string | null | undefined;
56
+ }[];
57
+ voiceNote?: boolean | undefined;
58
+ template?: {
59
+ name: string;
60
+ language: string;
61
+ } | null | undefined;
62
+ viaAssistant?: boolean | undefined;
63
+ transcript?: string | null | undefined;
64
+ transcriptLang?: string | null | undefined;
65
+ transcriptStatus?: "pending" | "failed" | "ok" | "too_long" | "unsupported" | "skipped" | null | undefined;
66
+ senderProfile?: {
67
+ chatJid: string;
68
+ name: string;
69
+ } | null | undefined;
70
+ }, Error, {
71
+ name: string;
72
+ language: string;
73
+ clientId: string;
74
+ params?: string[] | undefined;
75
+ }, unknown>;
@@ -0,0 +1,5 @@
1
+ import type { WebExtension, WebExtensionInboxType } from './types';
2
+ /** Every extension inbox type in extension order; the first extension wins a duplicate type id. */
3
+ export declare function inboxTypesOf(extensions: readonly WebExtension[]): WebExtensionInboxType[];
4
+ /** Inbox connection types the extensions add (none in Lite). */
5
+ export declare function useInboxTypes(): readonly WebExtensionInboxType[];
@@ -2,4 +2,5 @@ export * from './types';
2
2
  export { WebExtensionsProvider, useWebExtensions } from './context';
3
3
  export { registerExtensionCatalogs, useExtensionTranslation, type ExtensionTranslate, } from './i18n';
4
4
  export { ExtensionBoundary, extensionRoutes } from './routes';
5
+ export { inboxTypesOf, useInboxTypes } from './inbox-types';
5
6
  export { AgentSectionProvider, useAgentSection, type AgentSectionInfo } from './agent-section';
@@ -1,5 +1,5 @@
1
1
  import type { ComponentType, ReactNode } from 'react';
2
- import { type Locale } from '@ezychat/shared';
2
+ import { type Inbox, type Locale } from '@ezychat/shared';
3
3
  /**
4
4
  * Web extension API: lets another product build (e.g. Enterprise) add UI without forking Lite.
5
5
  *
@@ -88,6 +88,57 @@ export interface WebExtensionLockScreenProps {
88
88
  recheck: () => void;
89
89
  }
90
90
  export type WebExtensionCatalogs = Partial<Record<Locale, Record<string, string>>>;
91
+ /** The draft of the Add inbox dialog that a type's CreateForm receives. */
92
+ export interface WebExtensionInboxDraft {
93
+ name: string;
94
+ memberIds: number[];
95
+ aiAgentId: number | null;
96
+ }
97
+ /**
98
+ * The screens of an inbox connection type an extension adds on the server (`ServerExtension.inboxTypes`),
99
+ * e.g. the WhatsApp Business API. The core shows a type choice in Add inbox (linked phone first) and in
100
+ * the setup wizard's WhatsApp step, and places these components; the type's own routes do the work.
101
+ */
102
+ export interface WebExtensionInboxType {
103
+ /** The server inbox type id (`Inbox.type`). */
104
+ type: string;
105
+ /** Catalog keys (`ext.<name>.…`) of the choice card. */
106
+ labelKey: string;
107
+ descriptionKey: string;
108
+ icon?: ComponentType<{
109
+ className?: string;
110
+ 'aria-hidden'?: boolean;
111
+ }>;
112
+ /**
113
+ * Add inbox, after the core's name/members/AI agent fields: collects the type's details, creates the
114
+ * inbox through the extension's route, then calls `onCreated`. `validate()` checks the core fields
115
+ * first (shows their errors) and says whether they are valid.
116
+ */
117
+ CreateForm: ComponentType<{
118
+ draft: WebExtensionInboxDraft;
119
+ validate: () => boolean;
120
+ onCreated: (inbox: Inbox) => void;
121
+ }>;
122
+ /** Rows under an inbox of this type on its Admin → Inboxes card. */
123
+ CardDetails?: ComponentType<{
124
+ inbox: Inbox;
125
+ }>;
126
+ /** Edit dialog section for an inbox of this type (details, replace credentials, switch back). */
127
+ EditSection?: ComponentType<{
128
+ inbox: Inbox;
129
+ onChanged: () => void;
130
+ }>;
131
+ /** Edit dialog of a linked-phone inbox: the flow that moves it to this type. */
132
+ SwitchFromPhone?: ComponentType<{
133
+ inbox: Inbox;
134
+ onChanged: () => void;
135
+ onCancel: () => void;
136
+ }>;
137
+ /** Setup wizard WhatsApp step: connects inbox 1 with this type instead of the QR scan. */
138
+ SetupForm?: ComponentType<{
139
+ onConnected: () => void;
140
+ }>;
141
+ }
91
142
  export interface WebExtension {
92
143
  /** Lowercase id, 2–32 characters: `/^[a-z][a-z0-9-]{1,31}$/`; not `lite` or `core`. */
93
144
  name: string;
@@ -99,6 +150,8 @@ export interface WebExtension {
99
150
  banners?: ComponentType[];
100
151
  /** Full-page screen while access is closed; the first extension that has one wins. */
101
152
  lockScreen?: ComponentType<WebExtensionLockScreenProps>;
153
+ /** Inbox connection types (Admin → Inboxes, setup wizard); the first extension wins a duplicate. */
154
+ inboxTypes?: WebExtensionInboxType[];
102
155
  catalogs?: WebExtensionCatalogs;
103
156
  }
104
157
  export declare const EXTENSION_NAME_PATTERN: RegExp;
@@ -274,6 +274,9 @@ export declare const enResources: {
274
274
  step3: string;
275
275
  disclaimer: string;
276
276
  skip: string;
277
+ choice: string;
278
+ choicePhone: string;
279
+ choicePhoneHint: string;
277
280
  };
278
281
  ai: {
279
282
  title: string;
@@ -558,6 +561,7 @@ export declare const enResources: {
558
561
  lookup_failed: string;
559
562
  business_rule: string;
560
563
  confidential_leak: string;
564
+ window_closed: string;
561
565
  };
562
566
  resolved: string;
563
567
  reopenedByMessage: string;
@@ -622,6 +626,7 @@ export declare const enResources: {
622
626
  remove: string;
623
627
  fromTeammateUnknown: string;
624
628
  };
629
+ template: string;
625
630
  };
626
631
  media: {
627
632
  types: {
@@ -851,6 +856,44 @@ export declare const enResources: {
851
856
  add: string;
852
857
  hide: string;
853
858
  };
859
+ window: {
860
+ leftHours: string;
861
+ leftMinutes: string;
862
+ explain: string;
863
+ closedTitle: string;
864
+ closedBody: string;
865
+ closedShort: string;
866
+ sendTemplate: string;
867
+ sent: string;
868
+ dialogTitle: string;
869
+ dialogDescription: string;
870
+ loading: string;
871
+ loadFailed: string;
872
+ choose: string;
873
+ values: string;
874
+ variable: string;
875
+ required: string;
876
+ preview: string;
877
+ send: string;
878
+ marketingNote: string;
879
+ category: {
880
+ utility: string;
881
+ marketing: string;
882
+ authentication: string;
883
+ };
884
+ unsupported: {
885
+ image: string;
886
+ document: string;
887
+ video: string;
888
+ buttons: string;
889
+ other: string;
890
+ };
891
+ emptyTitle: string;
892
+ emptyBody: string;
893
+ manage: string;
894
+ sync: string;
895
+ synced: string;
896
+ };
854
897
  };
855
898
  admin: {
856
899
  title: string;
@@ -2151,6 +2194,7 @@ export declare const enResources: {
2151
2194
  emptyTitle: string;
2152
2195
  emptyBody: string;
2153
2196
  description: string;
2197
+ descriptionTypes: string;
2154
2198
  edit: string;
2155
2199
  remove: string;
2156
2200
  status: string;
@@ -2205,6 +2249,15 @@ export declare const enResources: {
2205
2249
  aiAgentMove: string;
2206
2250
  aiAgentReplace: string;
2207
2251
  aiAgentRemove: string;
2252
+ typeField: string;
2253
+ typePhoneHint: string;
2254
+ typeOther: string;
2255
+ dialogDescriptionOther: string;
2256
+ connectionType: string;
2257
+ switchTo: string;
2258
+ removeBodyOther: string;
2259
+ aiWindowNote: string;
2260
+ createdOther: string;
2208
2261
  };
2209
2262
  };
2210
2263
  errors: {
@@ -8,8 +8,10 @@ export interface ChatListItemProps {
8
8
  assignedToMe?: boolean;
9
9
  /** The chat's inbox, only while the user sees two or more inboxes. */
10
10
  inbox?: Pick<Inbox, 'name' | 'tone'>;
11
+ /** The chat's reply window has closed (WhatsApp Business API): only a template may be sent. */
12
+ windowClosed?: boolean;
11
13
  }
12
14
  export declare function GroupIcon({ className }: {
13
15
  className?: string;
14
16
  }): import("react").JSX.Element;
15
- export declare function ChatListItem({ chat, active, assigneeName, assignedToMe, inbox, }: ChatListItemProps): import("react").JSX.Element;
17
+ export declare function ChatListItem({ chat, active, assigneeName, assignedToMe, inbox, windowClosed, }: ChatListItemProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ /** Meta's page where a WhatsApp Business Account creates and manages its message templates. */
2
+ export declare const TEMPLATES_MANAGER_URL = "https://business.facebook.com/wa/manage/message-templates/";
3
+ /** The 24-hour customer-service window of a Business API chat: a quiet line above the composer. */
4
+ export declare function ReplyWindowNote({ lastInboundAt, windowHours, }: Readonly<{
5
+ lastInboundAt: number | null;
6
+ windowHours: number;
7
+ }>): import("react").JSX.Element | null;
8
+ /** Replaces the composer once the reply window has closed: only an approved template may go. */
9
+ export declare function WindowClosedPanel({ jid, inboxId, customerName, disabled, }: Readonly<{
10
+ jid: string;
11
+ inboxId: number;
12
+ customerName: string;
13
+ /** WhatsApp is offline: nothing can be sent. */
14
+ disabled?: boolean;
15
+ }>): import("react").JSX.Element;
16
+ /** The body with each {{n}} replaced by its value; an empty value keeps its placeholder. */
17
+ export declare function fillTemplate(body: string, values: readonly string[]): string;
18
+ /** Pick an approved template, fill its variables and see the message before sending it. */
19
+ export declare function TemplateDialog({ open, jid, inboxId, customerName, onClose, }: Readonly<{
20
+ open: boolean;
21
+ jid: string;
22
+ inboxId: number;
23
+ customerName: string;
24
+ onClose(): void;
25
+ }>): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { type Inbox, type InboxCapabilities } from '@ezychat/shared';
2
+ /**
3
+ * What an inbox's connection can do. An older server sends no capabilities: such an inbox is a
4
+ * linked phone (QR scan, no reply window).
5
+ */
6
+ export declare function inboxCapabilities(inbox: Partial<Pick<Inbox, 'capabilities'>>): InboxCapabilities;
7
+ /** The inbox's connection type; an older server sends none (linked phone). */
8
+ export declare function inboxTypeOf(inbox: Partial<Pick<Inbox, 'type'>>): string;
9
+ /**
10
+ * Milliseconds left in a chat's reply window: `null` when its inbox has no window (linked phone),
11
+ * `0` once it has closed or before the customer ever wrote (only a template may be sent then).
12
+ */
13
+ export declare function replyWindowLeft(lastInboundAt: number | null | undefined, windowHours: number | null | undefined, now: number): number | null;
14
+ /** True in the last hour of an open window (the note turns to the warning tone). */
15
+ export declare function replyWindowEnding(left: number): boolean;
16
+ /** The current time, updated every `intervalMs` (the window labels tick each minute). */
17
+ export declare function useNow(intervalMs?: number): number;
18
+ /** The reply window hours of a chat's inbox (removed inboxes included), or null without one. */
19
+ export declare function useReplyWindowHours(): (inboxId: number) => number | null;
20
+ /**
21
+ * Whether a chat's reply window is closed (`null`: its inbox has no window). Re-renders only when
22
+ * the window closes, not every minute, so a whole conversation is not redrawn while it ticks.
23
+ */
24
+ export declare function useWindowClosed(lastInboundAt: number | null | undefined, windowHours: number | null | undefined): boolean | null;
@@ -3,7 +3,7 @@
3
3
  * without forking Lite or deep-importing its modules.
4
4
  */
5
5
  export { createInboxApp, type CreateInboxAppOptions } from './create-app';
6
- export { EXTENSION_NAME_PATTERN, WEB_EXTENSION_ADMIN_SECTIONS, WebExtensionError, extensionKeyPrefix, extensionPath, useAgentSection, useExtensionTranslation, validateWebExtensions, type AgentSectionInfo, type ExtensionTranslate, type WebExtension, type WebExtensionAdminSection, type WebExtensionAgent, type WebExtensionAgentSection, type WebExtensionCatalogs, type WebExtensionLockScreenProps, type WebExtensionNavBadge, type WebExtensionNavItem, type WebExtensionRoute, type WebExtensionSettingsSection, } from './extensions';
6
+ export { EXTENSION_NAME_PATTERN, WEB_EXTENSION_ADMIN_SECTIONS, WebExtensionError, extensionKeyPrefix, extensionPath, useAgentSection, useExtensionTranslation, validateWebExtensions, type AgentSectionInfo, type ExtensionTranslate, type WebExtension, type WebExtensionAdminSection, type WebExtensionAgent, type WebExtensionAgentSection, type WebExtensionCatalogs, type WebExtensionInboxDraft, type WebExtensionInboxType, type WebExtensionLockScreenProps, type WebExtensionNavBadge, type WebExtensionNavItem, type WebExtensionRoute, type WebExtensionSettingsSection, } from './extensions';
7
7
  export { useAppLinks } from './lib/app-links';
8
8
  export { LITE_APP_LINKS, type AppLinks } from './lib/links';
9
9
  export { PausedScreen } from './access/AccessGate';
@@ -0,0 +1,12 @@
1
+ import type React from 'react';
2
+ /**
3
+ * The setup wizard's WhatsApp step. With an extension inbox type that can connect inbox 1 (e.g. the
4
+ * WhatsApp Business API), it starts with the choice between the QR scan (default) and that type;
5
+ * without one (Lite) it is the QR step unchanged.
6
+ */
7
+ export declare function WhatsAppStepChoice({ phoneStep, onContinue, onSkip, }: Readonly<{
8
+ /** The QR step (`WaLinkStep`). */
9
+ phoneStep: React.ReactNode;
10
+ onContinue(): void;
11
+ onSkip(): void;
12
+ }>): React.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezychat/web",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "description": "EzyChat core: the team inbox web app (React) as a library, with createInboxApp and its extension API.",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "build:lib": "node ../../scripts/build-web-lib.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@ezychat/shared": "0.1.55",
43
+ "@ezychat/shared": "0.1.57",
44
44
  "@fontsource-variable/inter": "^5.3.0",
45
45
  "@tanstack/react-query": "^5.104.5",
46
46
  "@types/react": "^19.3.0",