@doany-ai/inbox 0.0.0-stage → 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.
@@ -0,0 +1,86 @@
1
+ export function memoryStorage() {
2
+ const values = new Map();
3
+ return {
4
+ get: (key) => values.get(key) ?? null,
5
+ set: (key, value) => void values.set(key, value),
6
+ remove: (key) => void values.delete(key),
7
+ };
8
+ }
9
+ /** The browser's localStorage when it can be used, else memory for this page. */
10
+ export function defaultStorage() {
11
+ let store;
12
+ try {
13
+ store = globalThis.localStorage;
14
+ }
15
+ catch {
16
+ store = undefined; // blocked (private mode, a sandboxed frame)
17
+ }
18
+ if (!store)
19
+ return memoryStorage();
20
+ const web = store;
21
+ const fallback = memoryStorage();
22
+ return {
23
+ get(key) {
24
+ try {
25
+ return web.getItem(key) ?? fallback.get(key);
26
+ }
27
+ catch {
28
+ return fallback.get(key);
29
+ }
30
+ },
31
+ set(key, value) {
32
+ try {
33
+ web.setItem(key, value);
34
+ }
35
+ catch {
36
+ fallback.set(key, value);
37
+ }
38
+ },
39
+ remove(key) {
40
+ fallback.remove(key);
41
+ try {
42
+ web.removeItem(key);
43
+ }
44
+ catch {
45
+ // Nothing to remove without storage.
46
+ }
47
+ },
48
+ };
49
+ }
50
+ /** The visitor hid the privacy line: kept for the browser session. */
51
+ export const PRIVACY_DISMISSED_KEY = "doany_inbox_privacy_dismissed";
52
+ const sessionFallback = memoryStorage();
53
+ function sessionStore() {
54
+ try {
55
+ return globalThis.sessionStorage;
56
+ }
57
+ catch {
58
+ return undefined; // blocked (private mode, a sandboxed frame)
59
+ }
60
+ }
61
+ /** A mark kept for the browser session; for this page only where the session's storage cannot be used. */
62
+ export function sessionMark(key) {
63
+ try {
64
+ if (sessionStore()?.getItem(key) != null)
65
+ return true;
66
+ }
67
+ catch {
68
+ // The page's own copy below.
69
+ }
70
+ return sessionFallback.get(key) != null;
71
+ }
72
+ export function setSessionMark(key) {
73
+ try {
74
+ const store = sessionStore();
75
+ if (store) {
76
+ store.setItem(key, "1");
77
+ return;
78
+ }
79
+ }
80
+ catch {
81
+ // Kept for this page only.
82
+ }
83
+ sessionFallback.set(key, "1");
84
+ }
85
+ /** The newest team message shown with the panel open. Message ids are unique, so one key serves every chat. */
86
+ export const READ_KEY = "doany_inbox_read";
@@ -0,0 +1,189 @@
1
+ import type { DoanyClientLike } from "@doany-ai/app-core";
2
+ /** What the site's chat button reads (`GET /apps/{app_id}/inbox/config`). A disabled config carries only `enabled`. */
3
+ export interface InboxConfig {
4
+ enabled: boolean;
5
+ project_id?: string;
6
+ /** The button's words, set by the business (default `Ask us`). */
7
+ label?: string;
8
+ corner?: "right" | "left" | (string & {});
9
+ /** Path prefixes where the button is not offered. */
10
+ hidden_paths?: string[];
11
+ business?: {
12
+ name: string;
13
+ };
14
+ receptionist?: {
15
+ /** The AI receptionist's name (default `Annie`). */
16
+ name: string;
17
+ /** The business's own greeting, or null for the package's `greeting` label. */
18
+ greeting: string | null;
19
+ };
20
+ paths?: InboxPaths;
21
+ /** Up to four questions offered before the first message. Present only while the AI receptionist answers (`paths.ask`). */
22
+ suggested_questions?: InboxSuggestedQuestion[];
23
+ availability?: InboxAvailability;
24
+ /** The site's live hostnames: the only hosts a message's text links to. */
25
+ link_hosts?: string[];
26
+ /** Show "Powered by DoAny". */
27
+ branding?: boolean;
28
+ }
29
+ /**
30
+ * A question the chat offers before the first message: the business's own
31
+ * words (`text`), or a code the platform built from what the business has,
32
+ * which shows a label (`inboxSuggestedQuestionLabelKeys`).
33
+ */
34
+ export type InboxSuggestedQuestion = {
35
+ text: string;
36
+ code?: undefined;
37
+ } | {
38
+ code: "prices" | "book" | "hours" | "address" | (string & {});
39
+ text?: undefined;
40
+ };
41
+ export interface InboxPaths {
42
+ /** The AI receptionist answers. */
43
+ ask: boolean;
44
+ /** Something is bookable online; the row also needs the site's `bookHref`. */
45
+ book: boolean;
46
+ /** The business's phone, or null. */
47
+ call: string | null;
48
+ /** The business's contact email: shown only at a limit, never as a row. */
49
+ email: string | null;
50
+ /** Send a message to the team. */
51
+ message: boolean;
52
+ }
53
+ export interface InboxAvailability {
54
+ open: boolean;
55
+ /** When it next opens, in the business's own offset (`2026-11-16T09:00:00-05:00`). */
56
+ back_at: string | null;
57
+ /** The business's away notice, shown whenever it is set. */
58
+ away_notice: string | null;
59
+ /**
60
+ * How many minutes the team usually takes to answer (5, 15, 30, 60, 120,
61
+ * 240, 480 or 1440), or null until it has answered enough chats within its
62
+ * open hours. Shown as "Usually replies within" while the business is open.
63
+ */
64
+ replies_within: number | null;
65
+ }
66
+ export interface InboxMe {
67
+ /** The chat belongs to the signed-in account: its email is not shown. */
68
+ signed_in: boolean;
69
+ }
70
+ /** One line of the chat, as the client's browser may see it. */
71
+ export interface InboxMessage {
72
+ id: string;
73
+ author: "client" | "receptionist" | "team" | "system" | (string & {});
74
+ /** The team member's first name, or the receptionist's name. */
75
+ author_name: string | null;
76
+ kind: "text" | "event" | "offer" | (string & {});
77
+ /** Plain text. Null for an event. */
78
+ text: string | null;
79
+ /** For an event, `code` names it; each code maps to a label (`inboxEventLabelKeys`). */
80
+ payload: ({
81
+ code?: string;
82
+ } & Record<string, unknown>) | null;
83
+ occurred_at: string;
84
+ }
85
+ export interface InboxContact {
86
+ email: string;
87
+ name?: string;
88
+ /** E.164. */
89
+ phone?: string;
90
+ }
91
+ /** The first message of a chat. The SDK makes the token, the message id and the key, and keeps them with the body until the platform decides. */
92
+ export interface InboxStartParams {
93
+ text: string;
94
+ /** Required unless a verified site account is signed in. */
95
+ contact?: InboxContact;
96
+ /** `person`: the Send a message row. The AI does not answer. */
97
+ intent?: "ask" | "person";
98
+ pagePath?: string;
99
+ language?: string;
100
+ capabilities?: string[];
101
+ }
102
+ export interface InboxStartReceipt {
103
+ me: InboxMe;
104
+ message: InboxMessage;
105
+ reply_pending: boolean;
106
+ }
107
+ /** A first message whose answer is still owed: the SDK resends it unchanged. */
108
+ export interface InboxPendingStart {
109
+ text: string;
110
+ intent: "ask" | "person";
111
+ contact: InboxContact | null;
112
+ }
113
+ export interface InboxThreadPage {
114
+ messages: InboxMessage[];
115
+ /** Read after this next time. Opaque. */
116
+ next_cursor: string | null;
117
+ reply_pending: boolean;
118
+ paused: boolean;
119
+ /** The button was turned off: the thread stays readable, nothing can be sent. */
120
+ off: boolean;
121
+ me: InboxMe;
122
+ /** The cursor given could not be used: this page reads from the start. */
123
+ restarted?: boolean;
124
+ }
125
+ export interface InboxSendParams {
126
+ text: string;
127
+ /** The same id again retries a send whose answer was lost: the chat keeps one copy. */
128
+ clientMessageId?: string;
129
+ capabilities?: string[];
130
+ }
131
+ export interface InboxSendReceipt {
132
+ message: InboxMessage;
133
+ reply_pending: boolean;
134
+ }
135
+ /** The part of the SDK client this package calls. A real SDK client satisfies `InboxClient`. */
136
+ export interface InboxClient extends DoanyClientLike {
137
+ inbox: {
138
+ config(): Promise<InboxConfig>;
139
+ /** The chat this browser holds, or null. No request. */
140
+ current(): {
141
+ email: string | null;
142
+ } | null;
143
+ /** A first message still owed, or null. No request. */
144
+ pendingStart(): InboxPendingStart | null;
145
+ /** The first message; with no params, the one still owed. */
146
+ start(params?: InboxStartParams): Promise<InboxStartReceipt>;
147
+ messages(params?: {
148
+ cursor?: string | null;
149
+ }): Promise<InboxThreadPage>;
150
+ send(params: InboxSendParams): Promise<InboxSendReceipt>;
151
+ person(params?: {
152
+ text?: string;
153
+ }): Promise<void>;
154
+ seen(throughCursor: string): Promise<void>;
155
+ resume(linkToken: string): Promise<{
156
+ me: InboxMe;
157
+ }>;
158
+ /** Reports a booking made from an offer in this chat. */
159
+ bookings(params: {
160
+ registrationId: string;
161
+ accessToken: string;
162
+ }): Promise<void>;
163
+ newMessageId(): string;
164
+ /** Forgets this browser's chat and a first message still owed. No request. */
165
+ forget(): void;
166
+ };
167
+ }
168
+ /** What an offer card hands to `renderBooking`: the service, its option, and the day the receptionist named. */
169
+ export interface InboxBookingOffer {
170
+ service_setting_id: string;
171
+ catalog_item_variant_id: string;
172
+ /** A day in `timezone`, `YYYY-MM-DD`. */
173
+ date: string;
174
+ timezone: string;
175
+ }
176
+ /** A booking the site's flow made, as `BookingFlow`'s `onBooked` gives it (`@doany-ai/calendar`'s `BookingResult`). */
177
+ export interface InboxBooked {
178
+ /** `status` `pending_payment`: held until it is paid, not booked yet. */
179
+ registration?: {
180
+ id: string;
181
+ status?: string;
182
+ } | null;
183
+ access_token?: string | null;
184
+ }
185
+ /** The second argument of `renderBooking`. */
186
+ export interface InboxBookingHooks {
187
+ /** Pass to the flow's `onBooked`: the chat and the business's Inbox then show the booking. */
188
+ onBooked(result: InboxBooked): void;
189
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@doany-ai/inbox",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The chat button on a Doany site: a visitor asks, books or reaches a person, and the business answers from its Inbox. A hook with no DOM and a default widget",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./core": {
16
+ "types": "./dist/core.d.ts",
17
+ "import": "./dist/core.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "scripts": {
22
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "vitest run",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "@doany-ai/app-core": "^0.2.3",
29
+ "@doany-ai/sdk": "^0.5.14 || ^0.6.0",
30
+ "@tanstack/react-query": "^5.0.0",
31
+ "react": "^18.2.0 || ^19.0.0",
32
+ "react-dom": "^18.2.0 || ^19.0.0",
33
+ "react-router-dom": "^6.4.0 || ^7.0.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@doany-ai/sdk": {
37
+ "optional": true
38
+ },
39
+ "react-router-dom": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "dependencies": {
44
+ "lucide-react": "^0.475.0"
45
+ },
46
+ "devDependencies": {
47
+ "@doany-ai/app-core": "file:../doany-app-core",
48
+ "@doany-ai/sdk": "file:../doany-sdk",
49
+ "@tanstack/react-query": "^5.84.1",
50
+ "@testing-library/jest-dom": "^6.6.3",
51
+ "@testing-library/react": "^16.2.0",
52
+ "@types/node": "^22.20.4",
53
+ "@types/react": "^18.2.66",
54
+ "@types/react-dom": "^18.2.22",
55
+ "jsdom": "^26.0.0",
56
+ "react": "^18.2.0",
57
+ "react-dom": "^18.2.0",
58
+ "react-router-dom": "^6.26.0",
59
+ "typescript": "^5.8.2",
60
+ "vitest": "^3.0.0"
61
+ },
62
+ "license": "MIT"
63
+ }