@businessdash/sdk 0.9.60 → 0.9.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +991 -295
  2. package/assets/biab-sw.js +92 -0
  3. package/dist/account-core/index.d.ts +126 -0
  4. package/dist/account-core/index.d.ts.map +1 -0
  5. package/dist/account-core/index.js +191 -0
  6. package/dist/account-core/index.js.map +1 -0
  7. package/dist/account.d.ts +11 -0
  8. package/dist/account.d.ts.map +1 -0
  9. package/dist/account.js +11 -0
  10. package/dist/account.js.map +1 -0
  11. package/dist/auth-handler.d.ts +1 -1
  12. package/dist/auth-links.d.ts +33 -0
  13. package/dist/auth-links.d.ts.map +1 -0
  14. package/dist/auth-links.js +64 -0
  15. package/dist/auth-links.js.map +1 -0
  16. package/dist/blog-core/index.d.ts +112 -0
  17. package/dist/blog-core/index.d.ts.map +1 -0
  18. package/dist/blog-core/index.js +330 -0
  19. package/dist/blog-core/index.js.map +1 -0
  20. package/dist/blog.d.ts +11 -0
  21. package/dist/blog.d.ts.map +1 -0
  22. package/dist/blog.js +11 -0
  23. package/dist/blog.js.map +1 -0
  24. package/dist/chat-core/index.d.ts +97 -0
  25. package/dist/chat-core/index.d.ts.map +1 -0
  26. package/dist/chat-core/index.js +237 -0
  27. package/dist/chat-core/index.js.map +1 -0
  28. package/dist/chat.d.ts +11 -0
  29. package/dist/chat.d.ts.map +1 -0
  30. package/dist/chat.js +11 -0
  31. package/dist/chat.js.map +1 -0
  32. package/dist/checkout-core/index.d.ts +65 -0
  33. package/dist/checkout-core/index.d.ts.map +1 -0
  34. package/dist/checkout-core/index.js +159 -0
  35. package/dist/checkout-core/index.js.map +1 -0
  36. package/dist/checkout.d.ts +10 -0
  37. package/dist/checkout.d.ts.map +1 -0
  38. package/dist/checkout.js +10 -0
  39. package/dist/checkout.js.map +1 -0
  40. package/dist/cli.js +133 -25
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +622 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +888 -1
  45. package/dist/client.js.map +1 -1
  46. package/dist/consent-core/index.cjs +107 -0
  47. package/dist/consent-core/index.d.ts +103 -0
  48. package/dist/consent-core/index.d.ts.map +1 -0
  49. package/dist/consent-core/index.js +157 -0
  50. package/dist/consent-core/index.js.map +1 -0
  51. package/dist/contracts.d.ts +3526 -767
  52. package/dist/contracts.d.ts.map +1 -1
  53. package/dist/contracts.js +571 -4
  54. package/dist/contracts.js.map +1 -1
  55. package/dist/data-model-schema.d.ts +1 -1
  56. package/dist/data-model-schema.d.ts.map +1 -1
  57. package/dist/element.d.ts +30 -0
  58. package/dist/element.d.ts.map +1 -1
  59. package/dist/element.js +56 -0
  60. package/dist/element.js.map +1 -1
  61. package/dist/embed-protocol.d.ts +86 -0
  62. package/dist/embed-protocol.d.ts.map +1 -0
  63. package/dist/embed-protocol.js +60 -0
  64. package/dist/embed-protocol.js.map +1 -0
  65. package/dist/framework-support.d.ts +122 -0
  66. package/dist/framework-support.d.ts.map +1 -0
  67. package/dist/framework-support.js +323 -0
  68. package/dist/framework-support.js.map +1 -0
  69. package/dist/index.cjs +1355 -24
  70. package/dist/legal-core/index.cjs +115 -0
  71. package/dist/legal-core/index.d.ts +160 -0
  72. package/dist/legal-core/index.d.ts.map +1 -0
  73. package/dist/legal-core/index.js +220 -0
  74. package/dist/legal-core/index.js.map +1 -0
  75. package/dist/marketing-core/index.d.ts +66 -0
  76. package/dist/marketing-core/index.d.ts.map +1 -0
  77. package/dist/marketing-core/index.js +154 -0
  78. package/dist/marketing-core/index.js.map +1 -0
  79. package/dist/marketing.d.ts +7 -0
  80. package/dist/marketing.d.ts.map +1 -0
  81. package/dist/marketing.js +7 -0
  82. package/dist/marketing.js.map +1 -0
  83. package/dist/mcp-adapters/index.d.ts +135 -0
  84. package/dist/mcp-adapters/index.d.ts.map +1 -0
  85. package/dist/mcp-adapters/index.js +187 -0
  86. package/dist/mcp-adapters/index.js.map +1 -0
  87. package/dist/mcp-adapters.d.ts +7 -0
  88. package/dist/mcp-adapters.d.ts.map +1 -0
  89. package/dist/mcp-adapters.js +7 -0
  90. package/dist/mcp-adapters.js.map +1 -0
  91. package/dist/model3d-core/index.cjs +95 -0
  92. package/dist/model3d-core/index.d.ts +167 -0
  93. package/dist/model3d-core/index.d.ts.map +1 -0
  94. package/dist/model3d-core/index.js +158 -0
  95. package/dist/model3d-core/index.js.map +1 -0
  96. package/dist/notifications-core/index.cjs +315 -0
  97. package/dist/notifications-core/index.d.ts +229 -0
  98. package/dist/notifications-core/index.d.ts.map +1 -0
  99. package/dist/notifications-core/index.js +445 -0
  100. package/dist/notifications-core/index.js.map +1 -0
  101. package/dist/portal-core/index.d.ts +451 -0
  102. package/dist/portal-core/index.d.ts.map +1 -0
  103. package/dist/portal-core/index.js +819 -0
  104. package/dist/portal-core/index.js.map +1 -0
  105. package/dist/portal.d.ts +7 -0
  106. package/dist/portal.d.ts.map +1 -0
  107. package/dist/portal.js +7 -0
  108. package/dist/portal.js.map +1 -0
  109. package/dist/push-core/index.cjs +171 -0
  110. package/dist/push-core/index.d.ts +149 -0
  111. package/dist/push-core/index.d.ts.map +1 -0
  112. package/dist/push-core/index.js +231 -0
  113. package/dist/push-core/index.js.map +1 -0
  114. package/dist/react-marketing.d.ts +1 -17
  115. package/dist/react-marketing.d.ts.map +1 -1
  116. package/dist/react-marketing.js +4 -0
  117. package/dist/react-marketing.js.map +1 -1
  118. package/dist/react.d.ts +294 -101
  119. package/dist/react.d.ts.map +1 -1
  120. package/dist/react.js +302 -246
  121. package/dist/react.js.map +1 -1
  122. package/dist/seed-core/index.d.ts +354 -0
  123. package/dist/seed-core/index.d.ts.map +1 -0
  124. package/dist/seed-core/index.js +420 -0
  125. package/dist/seed-core/index.js.map +1 -0
  126. package/dist/seed.d.ts +7 -0
  127. package/dist/seed.d.ts.map +1 -0
  128. package/dist/seed.js +7 -0
  129. package/dist/seed.js.map +1 -0
  130. package/dist/seo-core/blog.d.ts +94 -0
  131. package/dist/seo-core/blog.d.ts.map +1 -0
  132. package/dist/seo-core/blog.js +149 -0
  133. package/dist/seo-core/blog.js.map +1 -0
  134. package/dist/seo-core/commerce.d.ts +64 -0
  135. package/dist/seo-core/commerce.d.ts.map +1 -0
  136. package/dist/seo-core/commerce.js +87 -0
  137. package/dist/seo-core/commerce.js.map +1 -0
  138. package/dist/seo-core/index.cjs +751 -0
  139. package/dist/seo-core/index.d.ts +226 -0
  140. package/dist/seo-core/index.d.ts.map +1 -0
  141. package/dist/seo-core/index.js +455 -0
  142. package/dist/seo-core/index.js.map +1 -0
  143. package/dist/seo-core/local.d.ts +181 -0
  144. package/dist/seo-core/local.d.ts.map +1 -0
  145. package/dist/seo-core/local.js +193 -0
  146. package/dist/seo-core/local.js.map +1 -0
  147. package/dist/sitemap-core/index.cjs +167 -0
  148. package/dist/sitemap-core/index.d.ts +155 -0
  149. package/dist/sitemap-core/index.d.ts.map +1 -0
  150. package/dist/sitemap-core/index.js +195 -0
  151. package/dist/sitemap-core/index.js.map +1 -0
  152. package/dist/social-core/index.d.ts +74 -0
  153. package/dist/social-core/index.d.ts.map +1 -0
  154. package/dist/social-core/index.js +72 -0
  155. package/dist/social-core/index.js.map +1 -0
  156. package/dist/social.d.ts +7 -0
  157. package/dist/social.d.ts.map +1 -0
  158. package/dist/social.js +7 -0
  159. package/dist/social.js.map +1 -0
  160. package/dist/solid-compat.d.ts +204 -0
  161. package/dist/solid-compat.d.ts.map +1 -0
  162. package/dist/solid-compat.js +221 -0
  163. package/dist/solid-compat.js.map +1 -0
  164. package/dist/store-core/index.d.ts +141 -0
  165. package/dist/store-core/index.d.ts.map +1 -0
  166. package/dist/store-core/index.js +277 -0
  167. package/dist/store-core/index.js.map +1 -0
  168. package/dist/store.d.ts +12 -0
  169. package/dist/store.d.ts.map +1 -0
  170. package/dist/store.js +12 -0
  171. package/dist/store.js.map +1 -0
  172. package/dist/vanilla.d.ts +111 -0
  173. package/dist/vanilla.d.ts.map +1 -1
  174. package/dist/vanilla.js +206 -0
  175. package/dist/vanilla.js.map +1 -1
  176. package/dist/verify-core/index.d.ts +98 -0
  177. package/dist/verify-core/index.d.ts.map +1 -0
  178. package/dist/verify-core/index.js +186 -0
  179. package/dist/verify-core/index.js.map +1 -0
  180. package/dist/verify.d.ts +8 -0
  181. package/dist/verify.d.ts.map +1 -0
  182. package/dist/verify.js +8 -0
  183. package/dist/verify.js.map +1 -0
  184. package/openapi.json +4516 -1311
  185. package/package.json +128 -11
  186. package/src/account-core/index.ts +309 -0
  187. package/src/account.ts +11 -0
  188. package/src/angular/headless.ts +519 -0
  189. package/src/angular/index.ts +3 -0
  190. package/src/auth-links.ts +76 -0
  191. package/src/blog-core/index.ts +423 -0
  192. package/src/blog.ts +11 -0
  193. package/src/chat-core/index.ts +335 -0
  194. package/src/chat.ts +11 -0
  195. package/src/checkout-core/index.ts +225 -0
  196. package/src/checkout.ts +10 -0
  197. package/src/cli.ts +184 -28
  198. package/src/client.ts +1097 -0
  199. package/src/consent-core/index.ts +213 -0
  200. package/src/contracts.ts +768 -11
  201. package/src/data-model-schema.ts +1 -1
  202. package/src/element.ts +102 -0
  203. package/src/embed-protocol.ts +100 -0
  204. package/src/framework-support.ts +440 -0
  205. package/src/legal-core/index.ts +276 -0
  206. package/src/marketing-core/index.ts +209 -0
  207. package/src/marketing.ts +7 -0
  208. package/src/mcp-adapters/index.ts +264 -0
  209. package/src/mcp-adapters.ts +7 -0
  210. package/src/model3d-core/index.ts +248 -0
  211. package/src/notifications-core/index.ts +650 -0
  212. package/src/portal-core/index.ts +1564 -0
  213. package/src/portal.ts +7 -0
  214. package/src/push-core/index.ts +335 -0
  215. package/src/qwik.tsx +602 -2
  216. package/src/react-marketing.tsx +5 -0
  217. package/src/react.tsx +434 -319
  218. package/src/seed-core/index.ts +735 -0
  219. package/src/seed.ts +7 -0
  220. package/src/seo-core/blog.ts +216 -0
  221. package/src/seo-core/commerce.ts +138 -0
  222. package/src/seo-core/index.ts +605 -0
  223. package/src/seo-core/local.ts +333 -0
  224. package/src/sitemap-core/index.ts +296 -0
  225. package/src/social-core/index.ts +103 -0
  226. package/src/social.ts +7 -0
  227. package/src/solid-compat.tsx +347 -0
  228. package/src/solid-surfaces.tsx +1311 -0
  229. package/src/solid.tsx +82 -60
  230. package/src/store-core/index.ts +426 -0
  231. package/src/store.ts +12 -0
  232. package/src/svelte/SocialLinks.svelte +84 -0
  233. package/src/svelte/headless.ts +435 -0
  234. package/src/svelte/index.ts +9 -0
  235. package/src/vanilla.ts +367 -0
  236. package/src/verify-core/index.ts +288 -0
  237. package/src/verify.ts +8 -0
  238. package/src/vue/headless.ts +527 -0
  239. package/src/vue/index.ts +3 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * `@businessdash/sdk/notifications` — one hook, every notification.
3
+ *
4
+ * ## The problem this solves
5
+ *
6
+ * BusinessDash raises a lot of notifications: an order ships, an invoice is
7
+ * paid, a technician is dispatched, staff reply to a message, a quote expires.
8
+ * Before this, a site that wanted to surface any of them had to know each one
9
+ * existed, find the call that returns it, poll it, diff it, and write a toast —
10
+ * per event type. Sites did two of them and missed the rest, and every new
11
+ * platform event was a change every consumer had to make.
12
+ *
13
+ * So the SDK inverts it. Attach ONE handler and receive everything, already
14
+ * normalised to a single shape:
15
+ *
16
+ * ```ts
17
+ * const feed = createNotificationFeed({ portal });
18
+ * feed.subscribe((n) => toast(n.title, { description: n.body }));
19
+ * feed.start();
20
+ * ```
21
+ *
22
+ * That is the entire integration. New platform events arrive through the same
23
+ * handler with no code change, because nothing in the consumer's code names an
24
+ * event type.
25
+ *
26
+ * ## We do not ship a toast
27
+ *
28
+ * Deliberately. A notification component is one of the most branded things on a
29
+ * site — position, motion, stacking, dismiss behaviour, dark mode — and an SDK
30
+ * that renders its own is one a designer has to fight. This produces normalised
31
+ * DATA and stops there; the consumer keeps their own component and their own
32
+ * design. `toToast()` flattens to the shape sonner, react-hot-toast, Vue Toast
33
+ * and friends all accept, for the common case where a straight mapping is all
34
+ * anyone wants.
35
+ *
36
+ * ## Where notifications actually come from
37
+ *
38
+ * Two sources, and they behave differently enough to be worth knowing:
39
+ *
40
+ * - **Push** (`pushSource`) — the service worker relays what it received to any
41
+ * open tab. Instant, but only while a tab is open; when the site is closed
42
+ * the OS notification IS the delivery and no toast is possible or wanted.
43
+ * - **Polling** (`portalSource`) — watches the signed-in customer's portal and
44
+ * raises a notification when something it tracks changes. Works without push
45
+ * permission, which matters because most visitors never grant it.
46
+ *
47
+ * Both feed one stream, and **the same event routinely arrives twice** — push
48
+ * is instant, the poll notices the same change seconds later. Toasting it twice
49
+ * is the obvious bug, so the feed deduplicates on a stable id before any
50
+ * handler sees it. That deduplication is the main reason this is a shared
51
+ * component rather than a snippet in a README.
52
+ *
53
+ * @module
54
+ */
55
+ import type { PortalController } from "../portal-core/index.js";
56
+ /** The six the platform sends. Open, because the list grows. */
57
+ export type NotificationCategory = "transactional" | "account" | "messages" | "product_updates" | "marketing" | "digest" | (string & {});
58
+ /**
59
+ * One notification, whatever it came from.
60
+ *
61
+ * The shape is deliberately small and toast-shaped. Anything specific to an
62
+ * event type lives in `data` rather than growing this — a consumer that never
63
+ * looks at `data` still renders every notification correctly, which is the
64
+ * whole promise.
65
+ */
66
+ export type BusinessDashNotification = {
67
+ /**
68
+ * Stable across sources. Two deliveries of one event share it, which is
69
+ * what makes deduplication work — see `dedupeKey` below for how it is
70
+ * derived when the source does not supply one.
71
+ */
72
+ id: string;
73
+ /** Where this delivery came from. Rarely interesting; occasionally is. */
74
+ source: "push" | "poll" | "local";
75
+ category: NotificationCategory;
76
+ /**
77
+ * A dotted event name — `order.shipped`, `invoice.paid`, `message.received`.
78
+ *
79
+ * Provided so a consumer CAN special-case one event. Nobody has to: the
80
+ * title and body are always populated.
81
+ */
82
+ kind: string;
83
+ title: string;
84
+ body: string | null;
85
+ /** Where the notification points. Render as the toast's action. */
86
+ url: string | null;
87
+ /**
88
+ * Maps to whatever a toast library calls its variants.
89
+ *
90
+ * `success` is used sparingly — a shipped order is `info`, not a
91
+ * celebration. `error` means something needs the customer to act.
92
+ */
93
+ severity: "info" | "success" | "warning" | "error";
94
+ /** ISO 8601. When the event happened, not when this tab noticed it. */
95
+ occurredAt: string;
96
+ /** Which company it came from. Null for platform-level notices. */
97
+ orgId: string | null;
98
+ /** Everything event-specific: order id, invoice number, ETA, and so on. */
99
+ data: Record<string, unknown>;
100
+ };
101
+ /** Everything except what the feed fills in. */
102
+ export type NotificationInput = Partial<BusinessDashNotification> & Pick<BusinessDashNotification, "title">;
103
+ export type NotificationHandler = (notification: BusinessDashNotification) => void;
104
+ /**
105
+ * A place notifications come from.
106
+ *
107
+ * Called with an `emit` and returns its own teardown. Consumers can write one
108
+ * to bring in a channel the SDK does not know about — a websocket, an SSE
109
+ * stream, their own backend — and it joins the same deduplicated feed.
110
+ */
111
+ export type NotificationSource = (emit: (input: NotificationInput) => void) => () => void;
112
+ export type NotificationFeedOptions = {
113
+ /**
114
+ * A portal controller to watch. Shorthand for adding `portalSource`.
115
+ *
116
+ * Optional: a marketing site with no signed-in customer still receives push
117
+ * notifications, and still wants them toasted.
118
+ */
119
+ portal?: PortalController;
120
+ /** How often to poll the portal. Default 60s; floor 15s. */
121
+ pollIntervalMs?: number;
122
+ /** Extra sources, including your own. */
123
+ sources?: NotificationSource[];
124
+ /**
125
+ * How long an id is remembered for deduplication. Default 5 minutes.
126
+ *
127
+ * Long enough to cover push and the next poll seeing the same event; short
128
+ * enough that a genuinely repeated notification hours later still shows.
129
+ */
130
+ dedupeWindowMs?: number;
131
+ /** How many to keep for `recent()`. Default 50. */
132
+ historyLimit?: number;
133
+ /**
134
+ * Suppress the OS notification when a tab is already focused.
135
+ *
136
+ * Off by default. A push MUST show a system notification — browsers revoke
137
+ * permission from sites that receive pushes silently — so this closes it
138
+ * immediately after, which is a different thing from never showing it and
139
+ * is visible as a brief flash on some platforms. Worth it when a toast has
140
+ * already said the same thing; not worth it otherwise.
141
+ */
142
+ closeOsNotificationWhenFocused?: boolean;
143
+ onError?: (error: Error) => void;
144
+ };
145
+ export type NotificationFeed = {
146
+ /**
147
+ * Attach a handler. Returns its own unsubscribe.
148
+ *
149
+ * **Past notifications are NOT replayed.** A toast is a moment, and
150
+ * replaying a morning's worth on every mount would fire a stack of stale
151
+ * toasts each time someone navigates. Use `recent()` to render a bell or an
152
+ * inbox, where showing history is the point.
153
+ */
154
+ subscribe(handler: NotificationHandler): () => void;
155
+ /**
156
+ * Raise a notification from your own code.
157
+ *
158
+ * It goes through the same deduplication and the same handlers, so an app
159
+ * can route its own messages through one component instead of two.
160
+ */
161
+ emit(input: NotificationInput): void;
162
+ /** Begin listening and polling. Idempotent. */
163
+ start(): void;
164
+ /** Stop, without dropping handlers. `start()` resumes. */
165
+ stop(): void;
166
+ /** Newest first, capped at `historyLimit`. */
167
+ recent(): BusinessDashNotification[];
168
+ /** Stop everything and drop handlers. */
169
+ destroy(): void;
170
+ };
171
+ /**
172
+ * Build the feed.
173
+ *
174
+ * Nothing runs until `start()`, so a controller can be constructed during
175
+ * render without a component mount kicking off a poll.
176
+ */
177
+ export declare function createNotificationFeed(options?: NotificationFeedOptions): NotificationFeed;
178
+ /**
179
+ * Push notifications relayed by the service worker, for tabs that are open.
180
+ *
181
+ * The worker shipped at `@businessdash/sdk/biab-sw.js` posts every push to its
182
+ * clients. A worker written by hand has to do the same — post
183
+ * `{ type: "businessdash:push", payload }` — or this source hears nothing.
184
+ */
185
+ export declare function pushSource(options?: {
186
+ closeOsNotificationWhenFocused?: boolean;
187
+ onError?: (error: Error) => void;
188
+ }): NotificationSource;
189
+ /**
190
+ * Notifications inferred from the signed-in customer's portal.
191
+ *
192
+ * ## Why polling, and why this is not a downgrade
193
+ *
194
+ * Push reaches maybe a third of visitors — the rest never grant permission, or
195
+ * are on an iPhone that has not been added to the Home Screen. A portal that
196
+ * only notified push subscribers would be silent for most customers. Polling
197
+ * covers everyone who has the page open, which is exactly the audience a toast
198
+ * can reach anyway.
199
+ *
200
+ * ## The first tick never notifies
201
+ *
202
+ * It records a baseline instead. Otherwise opening the portal would toast every
203
+ * order that ever shipped, which reads as a flood of stale news and trains
204
+ * people to dismiss without reading.
205
+ */
206
+ export declare function portalSource(options: {
207
+ portal: PortalController;
208
+ intervalMs?: number;
209
+ }): NotificationSource;
210
+ /**
211
+ * Flatten to the shape every toast library accepts.
212
+ *
213
+ * `sonner`, `react-hot-toast`, `vue-toastification`, `svelte-french-toast` and
214
+ * shadcn's `useToast` all take some arrangement of title, description, variant
215
+ * and an action — so this is the mapping most consumers would write by hand,
216
+ * offered once so they do not have to.
217
+ *
218
+ * Rendering stays entirely theirs. This returns data.
219
+ */
220
+ export declare function toToast(notification: BusinessDashNotification): {
221
+ title: string;
222
+ description: string | undefined;
223
+ variant: "default" | "success" | "warning" | "destructive";
224
+ action: {
225
+ label: string;
226
+ url: string;
227
+ } | undefined;
228
+ };
229
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications-core/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,445 @@
1
+ /**
2
+ * `@businessdash/sdk/notifications` — one hook, every notification.
3
+ *
4
+ * ## The problem this solves
5
+ *
6
+ * BusinessDash raises a lot of notifications: an order ships, an invoice is
7
+ * paid, a technician is dispatched, staff reply to a message, a quote expires.
8
+ * Before this, a site that wanted to surface any of them had to know each one
9
+ * existed, find the call that returns it, poll it, diff it, and write a toast —
10
+ * per event type. Sites did two of them and missed the rest, and every new
11
+ * platform event was a change every consumer had to make.
12
+ *
13
+ * So the SDK inverts it. Attach ONE handler and receive everything, already
14
+ * normalised to a single shape:
15
+ *
16
+ * ```ts
17
+ * const feed = createNotificationFeed({ portal });
18
+ * feed.subscribe((n) => toast(n.title, { description: n.body }));
19
+ * feed.start();
20
+ * ```
21
+ *
22
+ * That is the entire integration. New platform events arrive through the same
23
+ * handler with no code change, because nothing in the consumer's code names an
24
+ * event type.
25
+ *
26
+ * ## We do not ship a toast
27
+ *
28
+ * Deliberately. A notification component is one of the most branded things on a
29
+ * site — position, motion, stacking, dismiss behaviour, dark mode — and an SDK
30
+ * that renders its own is one a designer has to fight. This produces normalised
31
+ * DATA and stops there; the consumer keeps their own component and their own
32
+ * design. `toToast()` flattens to the shape sonner, react-hot-toast, Vue Toast
33
+ * and friends all accept, for the common case where a straight mapping is all
34
+ * anyone wants.
35
+ *
36
+ * ## Where notifications actually come from
37
+ *
38
+ * Two sources, and they behave differently enough to be worth knowing:
39
+ *
40
+ * - **Push** (`pushSource`) — the service worker relays what it received to any
41
+ * open tab. Instant, but only while a tab is open; when the site is closed
42
+ * the OS notification IS the delivery and no toast is possible or wanted.
43
+ * - **Polling** (`portalSource`) — watches the signed-in customer's portal and
44
+ * raises a notification when something it tracks changes. Works without push
45
+ * permission, which matters because most visitors never grant it.
46
+ *
47
+ * Both feed one stream, and **the same event routinely arrives twice** — push
48
+ * is instant, the poll notices the same change seconds later. Toasting it twice
49
+ * is the obvious bug, so the feed deduplicates on a stable id before any
50
+ * handler sees it. That deduplication is the main reason this is a shared
51
+ * component rather than a snippet in a README.
52
+ *
53
+ * @module
54
+ */
55
+ /**
56
+ * The id used for deduplication when a source does not supply one.
57
+ *
58
+ * Push carries a server-assigned id and this is not needed. Polling has no id
59
+ * to carry — it inferred the event by noticing a value changed — so the key is
60
+ * built from what identifies the event rather than the delivery: kind, org, and
61
+ * whatever the source put in `data.entityId`.
62
+ *
63
+ * `occurredAt` is deliberately NOT part of it. A poll timestamps at discovery,
64
+ * push at send; including either would make two deliveries of one event look
65
+ * like two events, which is exactly the bug this exists to prevent.
66
+ */
67
+ function dedupeKey(input) {
68
+ if (input.id)
69
+ return input.id;
70
+ const entity = typeof input.data?.entityId === "string" ? input.data.entityId : "";
71
+ return [input.kind ?? "unknown", input.orgId ?? "", entity, input.title].join("|");
72
+ }
73
+ /**
74
+ * Build the feed.
75
+ *
76
+ * Nothing runs until `start()`, so a controller can be constructed during
77
+ * render without a component mount kicking off a poll.
78
+ */
79
+ export function createNotificationFeed(options = {}) {
80
+ const handlers = new Set();
81
+ const history = [];
82
+ /** id → when it was seen, so the window can be pruned. */
83
+ const seen = new Map();
84
+ const teardowns = [];
85
+ const dedupeWindowMs = options.dedupeWindowMs ?? 5 * 60 * 1000;
86
+ const historyLimit = options.historyLimit ?? 50;
87
+ let running = false;
88
+ let destroyed = false;
89
+ function fail(cause) {
90
+ options.onError?.(cause instanceof Error ? cause : new Error(String(cause)));
91
+ }
92
+ function deliver(input) {
93
+ if (destroyed)
94
+ return;
95
+ const key = dedupeKey(input);
96
+ const now = Date.now();
97
+ // Prune first, so a long-lived page does not accumulate keys forever.
98
+ for (const [id, at] of seen) {
99
+ if (now - at > dedupeWindowMs)
100
+ seen.delete(id);
101
+ }
102
+ if (seen.has(key))
103
+ return;
104
+ seen.set(key, now);
105
+ const notification = {
106
+ id: input.id ?? key,
107
+ source: input.source ?? "local",
108
+ category: input.category ?? "transactional",
109
+ kind: input.kind ?? "notification",
110
+ title: input.title,
111
+ body: input.body ?? null,
112
+ url: input.url ?? null,
113
+ severity: input.severity ?? "info",
114
+ occurredAt: input.occurredAt ?? new Date(now).toISOString(),
115
+ orgId: input.orgId ?? null,
116
+ data: input.data ?? {},
117
+ };
118
+ history.unshift(notification);
119
+ if (history.length > historyLimit)
120
+ history.length = historyLimit;
121
+ for (const handler of handlers) {
122
+ // One handler throwing must not stop the others, or a broken toast
123
+ // in one component silences every notification on the page.
124
+ try {
125
+ handler(notification);
126
+ }
127
+ catch (cause) {
128
+ fail(cause);
129
+ }
130
+ }
131
+ }
132
+ return {
133
+ subscribe(handler) {
134
+ handlers.add(handler);
135
+ return () => {
136
+ handlers.delete(handler);
137
+ };
138
+ },
139
+ emit(input) {
140
+ deliver({ ...input, source: input.source ?? "local" });
141
+ },
142
+ start() {
143
+ if (running || destroyed)
144
+ return;
145
+ running = true;
146
+ const sources = [
147
+ pushSource({
148
+ ...(options.closeOsNotificationWhenFocused === undefined
149
+ ? {}
150
+ : {
151
+ closeOsNotificationWhenFocused: options.closeOsNotificationWhenFocused,
152
+ }),
153
+ onError: fail,
154
+ }),
155
+ ...(options.portal
156
+ ? [
157
+ portalSource({
158
+ portal: options.portal,
159
+ ...(options.pollIntervalMs === undefined
160
+ ? {}
161
+ : { intervalMs: options.pollIntervalMs }),
162
+ }),
163
+ ]
164
+ : []),
165
+ ...(options.sources ?? []),
166
+ ];
167
+ for (const source of sources) {
168
+ try {
169
+ teardowns.push(source(deliver));
170
+ }
171
+ catch (cause) {
172
+ fail(cause);
173
+ }
174
+ }
175
+ },
176
+ stop() {
177
+ running = false;
178
+ while (teardowns.length) {
179
+ try {
180
+ teardowns.pop()?.();
181
+ }
182
+ catch (cause) {
183
+ fail(cause);
184
+ }
185
+ }
186
+ },
187
+ recent() {
188
+ return [...history];
189
+ },
190
+ destroy() {
191
+ this.stop();
192
+ destroyed = true;
193
+ handlers.clear();
194
+ history.length = 0;
195
+ seen.clear();
196
+ },
197
+ };
198
+ }
199
+ /**
200
+ * Push notifications relayed by the service worker, for tabs that are open.
201
+ *
202
+ * The worker shipped at `@businessdash/sdk/biab-sw.js` posts every push to its
203
+ * clients. A worker written by hand has to do the same — post
204
+ * `{ type: "businessdash:push", payload }` — or this source hears nothing.
205
+ */
206
+ export function pushSource(options = {}) {
207
+ return (emit) => {
208
+ if (typeof navigator === "undefined" ||
209
+ !("serviceWorker" in navigator) ||
210
+ typeof window === "undefined") {
211
+ return () => undefined;
212
+ }
213
+ const listener = (event) => {
214
+ const message = event.data;
215
+ if (message?.type !== "businessdash:push" || !message.payload)
216
+ return;
217
+ const payload = message.payload;
218
+ const data = typeof payload.data === "object" && payload.data !== null
219
+ ? payload.data
220
+ : {};
221
+ emit({
222
+ ...(typeof payload.id === "string" ? { id: payload.id } : {}),
223
+ source: "push",
224
+ ...(typeof payload.category === "string"
225
+ ? { category: payload.category }
226
+ : {}),
227
+ ...(typeof payload.kind === "string" ? { kind: payload.kind } : {}),
228
+ title: typeof payload.title === "string" ? payload.title : "Notification",
229
+ body: typeof payload.body === "string" ? payload.body : null,
230
+ url: typeof payload.url === "string" ? payload.url : null,
231
+ ...(typeof payload.orgId === "string" ? { orgId: payload.orgId } : {}),
232
+ ...(typeof payload.occurredAt === "string"
233
+ ? { occurredAt: payload.occurredAt }
234
+ : {}),
235
+ data,
236
+ });
237
+ if (options.closeOsNotificationWhenFocused &&
238
+ document.visibilityState === "visible") {
239
+ // The system notification has already been shown — required, and
240
+ // not optional. Closing it here trades a brief flash for not
241
+ // saying the same thing twice to someone who is looking at the
242
+ // page. See `closeOsNotificationWhenFocused` for why it is off
243
+ // by default.
244
+ navigator.serviceWorker.ready
245
+ .then((registration) => registration.getNotifications(typeof payload.tag === "string" ? { tag: payload.tag } : {}))
246
+ .then((notifications) => {
247
+ for (const notification of notifications)
248
+ notification.close();
249
+ })
250
+ .catch((cause) => options.onError?.(cause));
251
+ }
252
+ };
253
+ navigator.serviceWorker.addEventListener("message", listener);
254
+ return () => {
255
+ navigator.serviceWorker.removeEventListener("message", listener);
256
+ };
257
+ };
258
+ }
259
+ /**
260
+ * Notifications inferred from the signed-in customer's portal.
261
+ *
262
+ * ## Why polling, and why this is not a downgrade
263
+ *
264
+ * Push reaches maybe a third of visitors — the rest never grant permission, or
265
+ * are on an iPhone that has not been added to the Home Screen. A portal that
266
+ * only notified push subscribers would be silent for most customers. Polling
267
+ * covers everyone who has the page open, which is exactly the audience a toast
268
+ * can reach anyway.
269
+ *
270
+ * ## The first tick never notifies
271
+ *
272
+ * It records a baseline instead. Otherwise opening the portal would toast every
273
+ * order that ever shipped, which reads as a flood of stale news and trains
274
+ * people to dismiss without reading.
275
+ */
276
+ export function portalSource(options) {
277
+ // Floored rather than trusted: this runs against a rate-limited API, and a
278
+ // consumer passing 1000 because it "feels responsive" would spend an org's
279
+ // quota on a page nobody is looking at.
280
+ const intervalMs = Math.max(15_000, options.intervalMs ?? 60_000);
281
+ return (emit) => {
282
+ let stopped = false;
283
+ let baseline = null;
284
+ let timer = null;
285
+ function readWatermark() {
286
+ const snapshot = options.portal.snapshot();
287
+ return {
288
+ unreadMessages: snapshot.unreadMessageCount,
289
+ orderStatuses: new Map((snapshot.orders?.items ?? []).map((o) => [o.id, String(o.status)])),
290
+ invoiceStatuses: new Map((snapshot.invoices?.items ?? []).map((i) => [i.id, String(i.status)])),
291
+ shipmentStatuses: new Map((snapshot.shipments?.items ?? []).map((sh) => [
292
+ sh.id,
293
+ String(sh.status ?? "unknown"),
294
+ ])),
295
+ };
296
+ }
297
+ function diff(previous, next) {
298
+ const orgId = options.portal.snapshot().context?.organization?.id ?? null;
299
+ if (next.unreadMessages > previous.unreadMessages) {
300
+ const delta = next.unreadMessages - previous.unreadMessages;
301
+ emit({
302
+ source: "poll",
303
+ category: "messages",
304
+ kind: "message.received",
305
+ title: delta === 1 ? "New message" : `${delta} new messages`,
306
+ body: "You have a new message from the team.",
307
+ severity: "info",
308
+ orgId,
309
+ // Keyed on the resulting count rather than the delta, so two
310
+ // tabs polling at different moments agree on the id.
311
+ data: { entityId: `unread:${next.unreadMessages}` },
312
+ });
313
+ }
314
+ for (const [id, status] of next.orderStatuses) {
315
+ const before = previous.orderStatuses.get(id);
316
+ // A newly appearing order is not a status CHANGE — it is an order
317
+ // this tab had not loaded yet, and announcing it would toast the
318
+ // backlog on first pagination.
319
+ if (before === undefined || before === status)
320
+ continue;
321
+ emit({
322
+ source: "poll",
323
+ category: "transactional",
324
+ kind: `order.${status}`,
325
+ title: orderTitle(status),
326
+ body: null,
327
+ severity: status === "cancelled" ? "warning" : "info",
328
+ orgId,
329
+ data: { entityId: `order:${id}:${status}`, orderId: id, status },
330
+ });
331
+ }
332
+ for (const [id, status] of next.invoiceStatuses) {
333
+ const before = previous.invoiceStatuses.get(id);
334
+ if (before === undefined || before === status)
335
+ continue;
336
+ emit({
337
+ source: "poll",
338
+ category: "transactional",
339
+ kind: `invoice.${status}`,
340
+ title: status === "paid"
341
+ ? "Invoice paid"
342
+ : status === "overdue"
343
+ ? "Invoice overdue"
344
+ : "Invoice updated",
345
+ body: null,
346
+ severity: status === "overdue" ? "error" : "info",
347
+ orgId,
348
+ data: { entityId: `invoice:${id}:${status}`, invoiceId: id, status },
349
+ });
350
+ }
351
+ for (const [id, status] of next.shipmentStatuses) {
352
+ const before = previous.shipmentStatuses.get(id);
353
+ if (before === undefined || before === status)
354
+ continue;
355
+ emit({
356
+ source: "poll",
357
+ category: "transactional",
358
+ kind: `shipment.${status}`,
359
+ title: status === "delivered"
360
+ ? "Your order was delivered"
361
+ : status === "out_for_delivery"
362
+ ? "Out for delivery"
363
+ : "Shipping update",
364
+ body: null,
365
+ severity: "info",
366
+ orgId,
367
+ data: {
368
+ entityId: `shipment:${id}:${status}`,
369
+ shipmentId: id,
370
+ status,
371
+ },
372
+ });
373
+ }
374
+ }
375
+ async function tick() {
376
+ if (stopped)
377
+ return;
378
+ // Refresh only the slices being watched, and settle rather than
379
+ // race: one failing call must not stop the other three from
380
+ // noticing their own changes.
381
+ await Promise.allSettled([
382
+ options.portal.loadOrders(),
383
+ options.portal.loadInvoices(),
384
+ options.portal.loadShipments(),
385
+ options.portal.loadMessages(),
386
+ ]);
387
+ if (stopped)
388
+ return;
389
+ const next = readWatermark();
390
+ if (baseline)
391
+ diff(baseline, next);
392
+ baseline = next;
393
+ }
394
+ // Baseline immediately from whatever is already loaded, so a change that
395
+ // happens before the first interval is still caught.
396
+ baseline = readWatermark();
397
+ timer = setInterval(() => void tick(), intervalMs);
398
+ return () => {
399
+ stopped = true;
400
+ if (timer)
401
+ clearInterval(timer);
402
+ };
403
+ };
404
+ }
405
+ function orderTitle(status) {
406
+ switch (status) {
407
+ case "shipped":
408
+ return "Your order shipped";
409
+ case "delivered":
410
+ return "Your order was delivered";
411
+ case "cancelled":
412
+ return "Your order was cancelled";
413
+ case "refunded":
414
+ return "Your refund was issued";
415
+ default:
416
+ return "Order updated";
417
+ }
418
+ }
419
+ /**
420
+ * Flatten to the shape every toast library accepts.
421
+ *
422
+ * `sonner`, `react-hot-toast`, `vue-toastification`, `svelte-french-toast` and
423
+ * shadcn's `useToast` all take some arrangement of title, description, variant
424
+ * and an action — so this is the mapping most consumers would write by hand,
425
+ * offered once so they do not have to.
426
+ *
427
+ * Rendering stays entirely theirs. This returns data.
428
+ */
429
+ export function toToast(notification) {
430
+ return {
431
+ title: notification.title,
432
+ description: notification.body ?? undefined,
433
+ variant: notification.severity === "error"
434
+ ? "destructive"
435
+ : notification.severity === "warning"
436
+ ? "warning"
437
+ : notification.severity === "success"
438
+ ? "success"
439
+ : "default",
440
+ action: notification.url
441
+ ? { label: "View", url: notification.url }
442
+ : undefined,
443
+ };
444
+ }
445
+ //# sourceMappingURL=index.js.map