@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
package/src/portal.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `@businessdash/sdk/portal` — the framework-agnostic customer-portal core.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ export * from "./portal-core/index.js";
@@ -0,0 +1,335 @@
1
+ /**
2
+ * `@businessdash/sdk/push-core` — browser push, from the org's own site.
3
+ *
4
+ * ## The one architectural fact that shapes all of this
5
+ *
6
+ * A push subscription belongs to an **origin**. The service worker must be
7
+ * served from the domain the visitor is actually on, so a subscription minted
8
+ * on the platform's domain cannot deliver to someone browsing the org's site.
9
+ * That is why this lives in the SDK rather than in the dashboard: only code
10
+ * running on the org's own origin can register it.
11
+ *
12
+ * What does NOT have to be per-origin is the signing key. `applicationServerKey`
13
+ * only has to match whatever signs the push, so **one platform VAPID pair
14
+ * serves every org** — no per-org registration, no per-org credentials, nothing
15
+ * for an org to configure.
16
+ *
17
+ * ## Safari
18
+ *
19
+ * Safari 16+ (macOS Ventura) and iOS 16.4+ implement the standard Web Push API
20
+ * with VAPID — the same one used here. Apple's old Safari Push flow (a Website
21
+ * Push ID registered in the developer portal, a signed push package listing
22
+ * allowed domains) applies to macOS Safari 15 and earlier and is not used.
23
+ *
24
+ * **iOS has a real constraint though:** push only works when the site has been
25
+ * added to the Home Screen. A plain Safari tab on iPhone cannot receive push at
26
+ * all, whatever anyone configures. `pushSupport()` reports that case
27
+ * specifically so a UI can explain it rather than showing a button that
28
+ * silently fails.
29
+ *
30
+ * ## Permission is a one-shot resource
31
+ *
32
+ * A browser asks once per origin. A refusal is sticky and expensive to undo —
33
+ * the visitor has to find it in site settings — so `subscribeToPush()` must be
34
+ * called from a user gesture, after the person has been told what they are
35
+ * agreeing to. Prompting on page load burns the permission for that origin,
36
+ * permanently, for everyone who says no reflexively.
37
+ *
38
+ * @module
39
+ */
40
+
41
+ /** What this browser can actually do. */
42
+ export type PushSupport =
43
+ | { supported: true }
44
+ | {
45
+ supported: false;
46
+ /**
47
+ * `unsupported` — no service worker or Push API at all.
48
+ * `ios-needs-home-screen` — iOS Safari in a normal tab; the site must
49
+ * be added to the Home Screen first. Worth its own case because it
50
+ * is fixable by the visitor, unlike the others.
51
+ * `insecure-context` — not HTTPS (localhost aside).
52
+ * `server` — no `window`; nothing to do.
53
+ */
54
+ reason:
55
+ | "unsupported"
56
+ | "ios-needs-home-screen"
57
+ | "insecure-context"
58
+ | "server";
59
+ message: string;
60
+ };
61
+
62
+ /** The bits of a `PushSubscription` the server needs, already base64url. */
63
+ export type PushSubscriptionPayload = {
64
+ endpoint: string;
65
+ p256dh: string;
66
+ auth: string;
67
+ userAgent?: string;
68
+ };
69
+
70
+ /** A browser this person has registered. */
71
+ export type PushDevice = {
72
+ id: string;
73
+ endpoint: string;
74
+ userAgent: string | null;
75
+ label: string | null;
76
+ categories: string[] | null;
77
+ lastSeenAt: string | null;
78
+ createdAt: string;
79
+ };
80
+
81
+ export type PushConfig = {
82
+ vapidPublicKey: string | null;
83
+ enabled: boolean;
84
+ devices: PushDevice[];
85
+ };
86
+
87
+ /**
88
+ * Can this browser receive push, and if not, why?
89
+ *
90
+ * Checked before rendering anything, so a visitor is never offered a button
91
+ * that cannot work. The iOS case in particular deserves an explanation rather
92
+ * than silence — "add this to your Home Screen first" is actionable, a dead
93
+ * button is not.
94
+ */
95
+ export function pushSupport(): PushSupport {
96
+ if (typeof window === "undefined") {
97
+ return { supported: false, reason: "server", message: "Not in a browser." };
98
+ }
99
+ if (!window.isSecureContext) {
100
+ return {
101
+ supported: false,
102
+ reason: "insecure-context",
103
+ message: "Push needs HTTPS.",
104
+ };
105
+ }
106
+ if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
107
+ // iOS Safari in a normal tab reports exactly this, so distinguish it —
108
+ // the visitor can fix theirs, and telling them "unsupported" is wrong.
109
+ if (isIosSafari() && !isStandalone()) {
110
+ return {
111
+ supported: false,
112
+ reason: "ios-needs-home-screen",
113
+ message:
114
+ "On iPhone and iPad, add this site to your Home Screen to turn on notifications.",
115
+ };
116
+ }
117
+ return {
118
+ supported: false,
119
+ reason: "unsupported",
120
+ message: "This browser doesn't support push notifications.",
121
+ };
122
+ }
123
+ if (isIosSafari() && !isStandalone()) {
124
+ return {
125
+ supported: false,
126
+ reason: "ios-needs-home-screen",
127
+ message:
128
+ "On iPhone and iPad, add this site to your Home Screen to turn on notifications.",
129
+ };
130
+ }
131
+ return { supported: true };
132
+ }
133
+
134
+ function isIosSafari(): boolean {
135
+ const ua = navigator.userAgent;
136
+ // iPadOS reports as Macintosh, so touch points are the tell.
137
+ const iOS =
138
+ /iPad|iPhone|iPod/.test(ua) ||
139
+ (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
140
+ return iOS && !/CriOS|FxiOS|EdgiOS/.test(ua);
141
+ }
142
+
143
+ function isStandalone(): boolean {
144
+ return (
145
+ window.matchMedia?.("(display-mode: standalone)").matches === true ||
146
+ (navigator as { standalone?: boolean }).standalone === true
147
+ );
148
+ }
149
+
150
+ /** The permission state, without asking for it. */
151
+ export function pushPermission(): NotificationPermission | "unsupported" {
152
+ if (typeof Notification === "undefined") return "unsupported";
153
+ return Notification.permission;
154
+ }
155
+
156
+ /**
157
+ * VAPID keys are base64url; `applicationServerKey` wants raw bytes.
158
+ *
159
+ * Backed by an explicit `ArrayBuffer` rather than `new Uint8Array(length)`:
160
+ * TypeScript's DOM types want a `BufferSource`, and a `Uint8Array` over the
161
+ * generic `ArrayBufferLike` (which could be a `SharedArrayBuffer`) does not
162
+ * satisfy it.
163
+ */
164
+ function urlBase64ToUint8Array(base64: string): Uint8Array<ArrayBuffer> {
165
+ const padding = "=".repeat((4 - (base64.length % 4)) % 4);
166
+ const normalised = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
167
+ const raw = atob(normalised);
168
+ const out = new Uint8Array(new ArrayBuffer(raw.length));
169
+ for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
170
+ return out;
171
+ }
172
+
173
+ /** …and the reverse, for the keys the browser hands back. */
174
+ function arrayBufferToBase64Url(buffer: ArrayBuffer | null): string {
175
+ if (!buffer) return "";
176
+ const bytes = new Uint8Array(buffer);
177
+ let binary = "";
178
+ for (const b of bytes) binary += String.fromCharCode(b);
179
+ return btoa(binary)
180
+ .replace(/\+/g, "-")
181
+ .replace(/\//g, "_")
182
+ .replace(/=+$/, "");
183
+ }
184
+
185
+ /** Flatten a `PushSubscription` into what the API stores. */
186
+ export function toPayload(
187
+ subscription: PushSubscription,
188
+ ): PushSubscriptionPayload {
189
+ return {
190
+ endpoint: subscription.endpoint,
191
+ p256dh: arrayBufferToBase64Url(subscription.getKey("p256dh")),
192
+ auth: arrayBufferToBase64Url(subscription.getKey("auth")),
193
+ ...(typeof navigator === "undefined"
194
+ ? {}
195
+ : { userAgent: navigator.userAgent }),
196
+ };
197
+ }
198
+
199
+ export type SubscribeOptions = {
200
+ /** From `GET customer-portal/push`. */
201
+ vapidPublicKey: string;
202
+ /**
203
+ * Path to the service worker ON YOUR ORIGIN. Defaults to `/biab-sw.js`.
204
+ *
205
+ * It must be served from your own domain — a worker on someone else's
206
+ * cannot receive your visitors' push. Copy the one this SDK ships.
207
+ */
208
+ serviceWorkerPath?: string;
209
+ /**
210
+ * Scope the worker controls. Defaults to `/`.
211
+ *
212
+ * A worker served from a subdirectory can only claim that subdirectory
213
+ * unless the response carries `Service-Worker-Allowed`, which is why the
214
+ * default file sits at the root.
215
+ */
216
+ scope?: string;
217
+ };
218
+
219
+ export type SubscribeResult =
220
+ | { ok: true; subscription: PushSubscriptionPayload }
221
+ | {
222
+ ok: false;
223
+ /**
224
+ * `denied` is the expensive one: the browser will not ask again, and
225
+ * the visitor has to undo it in site settings. Say so rather than
226
+ * offering the button a second time.
227
+ */
228
+ reason: "unsupported" | "denied" | "dismissed" | "failed";
229
+ message: string;
230
+ };
231
+
232
+ /**
233
+ * Register the service worker, ask for permission, and subscribe.
234
+ *
235
+ * **Call this from a click**, never on load. Permission is asked once per
236
+ * origin and a refusal is sticky, so prompting before the visitor knows what
237
+ * they are agreeing to spends the only chance you get.
238
+ *
239
+ * The result is a payload to POST to `customer-portal/push`; this function
240
+ * deliberately does not send it, so the caller keeps control of the request
241
+ * (its own auth, its own error handling, its own retry).
242
+ */
243
+ export async function subscribeToPush(
244
+ options: SubscribeOptions,
245
+ ): Promise<SubscribeResult> {
246
+ const support = pushSupport();
247
+ if (!support.supported) {
248
+ return { ok: false, reason: "unsupported", message: support.message };
249
+ }
250
+ if (Notification.permission === "denied") {
251
+ return {
252
+ ok: false,
253
+ reason: "denied",
254
+ message:
255
+ "Notifications are blocked for this site. They have to be re-enabled in the browser's site settings.",
256
+ };
257
+ }
258
+
259
+ try {
260
+ const registration = await navigator.serviceWorker.register(
261
+ options.serviceWorkerPath ?? "/biab-sw.js",
262
+ { scope: options.scope ?? "/" },
263
+ );
264
+ // A worker that is installing cannot take a subscription yet.
265
+ await navigator.serviceWorker.ready;
266
+
267
+ const permission = await Notification.requestPermission();
268
+ if (permission !== "granted") {
269
+ return {
270
+ ok: false,
271
+ // "default" means dismissed — the browser WILL ask again, so this
272
+ // is recoverable and should read differently from a block.
273
+ reason: permission === "denied" ? "denied" : "dismissed",
274
+ message:
275
+ permission === "denied"
276
+ ? "Notifications were blocked for this site."
277
+ : "No answer given — you can turn notifications on any time.",
278
+ };
279
+ }
280
+
281
+ // An existing subscription is reused rather than replaced: re-subscribing
282
+ // mints a new endpoint and orphans the old row.
283
+ const existing = await registration.pushManager.getSubscription();
284
+ const subscription =
285
+ existing ??
286
+ (await registration.pushManager.subscribe({
287
+ // Required by every browser now — a subscription without it is
288
+ // rejected outright rather than delivered to nobody.
289
+ userVisibleOnly: true,
290
+ applicationServerKey: urlBase64ToUint8Array(options.vapidPublicKey),
291
+ }));
292
+
293
+ return { ok: true, subscription: toPayload(subscription) };
294
+ } catch (error) {
295
+ return {
296
+ ok: false,
297
+ reason: "failed",
298
+ message:
299
+ error instanceof Error ? error.message : "Could not subscribe to push.",
300
+ };
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Tear the local subscription down and return its endpoint.
306
+ *
307
+ * The endpoint is what the server keys on, and a browser unsubscribing itself
308
+ * knows its endpoint rather than our row id — so hand this to
309
+ * `DELETE customer-portal/push` to remove the matching row. Dropping only the
310
+ * local one leaves the server pushing into a dead endpoint until the push
311
+ * service reports it gone.
312
+ */
313
+ export async function unsubscribeFromPush(): Promise<{
314
+ endpoint: string | null;
315
+ }> {
316
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
317
+ return { endpoint: null };
318
+ }
319
+ const registration = await navigator.serviceWorker.getRegistration();
320
+ const subscription = await registration?.pushManager.getSubscription();
321
+ if (!subscription) return { endpoint: null };
322
+ const { endpoint } = subscription;
323
+ await subscription.unsubscribe();
324
+ return { endpoint };
325
+ }
326
+
327
+ /** The current subscription, if this browser already has one. */
328
+ export async function currentPushSubscription(): Promise<PushSubscriptionPayload | null> {
329
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
330
+ return null;
331
+ }
332
+ const registration = await navigator.serviceWorker.getRegistration();
333
+ const subscription = await registration?.pushManager.getSubscription();
334
+ return subscription ? toPayload(subscription) : null;
335
+ }