@businessdash/sdk 0.9.60 → 0.9.70

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 (233) hide show
  1. package/README.md +1012 -186
  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 +567 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +807 -1
  45. package/dist/client.js.map +1 -1
  46. package/dist/contracts.d.ts +3309 -761
  47. package/dist/contracts.d.ts.map +1 -1
  48. package/dist/contracts.js +541 -4
  49. package/dist/contracts.js.map +1 -1
  50. package/dist/data-model-schema.d.ts +1 -1
  51. package/dist/data-model-schema.d.ts.map +1 -1
  52. package/dist/element.d.ts +30 -0
  53. package/dist/element.d.ts.map +1 -1
  54. package/dist/element.js +56 -0
  55. package/dist/element.js.map +1 -1
  56. package/dist/embed-protocol.d.ts +86 -0
  57. package/dist/embed-protocol.d.ts.map +1 -0
  58. package/dist/embed-protocol.js +60 -0
  59. package/dist/embed-protocol.js.map +1 -0
  60. package/dist/framework-support.d.ts +122 -0
  61. package/dist/framework-support.d.ts.map +1 -0
  62. package/dist/framework-support.js +323 -0
  63. package/dist/framework-support.js.map +1 -0
  64. package/dist/index.cjs +1244 -5
  65. package/dist/legal-core/index.cjs +115 -0
  66. package/dist/legal-core/index.d.ts +160 -0
  67. package/dist/legal-core/index.d.ts.map +1 -0
  68. package/dist/legal-core/index.js +220 -0
  69. package/dist/legal-core/index.js.map +1 -0
  70. package/dist/marketing-core/index.d.ts +66 -0
  71. package/dist/marketing-core/index.d.ts.map +1 -0
  72. package/dist/marketing-core/index.js +154 -0
  73. package/dist/marketing-core/index.js.map +1 -0
  74. package/dist/marketing.d.ts +7 -0
  75. package/dist/marketing.d.ts.map +1 -0
  76. package/dist/marketing.js +7 -0
  77. package/dist/marketing.js.map +1 -0
  78. package/dist/mcp-adapters/index.d.ts +135 -0
  79. package/dist/mcp-adapters/index.d.ts.map +1 -0
  80. package/dist/mcp-adapters/index.js +187 -0
  81. package/dist/mcp-adapters/index.js.map +1 -0
  82. package/dist/mcp-adapters.d.ts +7 -0
  83. package/dist/mcp-adapters.d.ts.map +1 -0
  84. package/dist/mcp-adapters.js +7 -0
  85. package/dist/mcp-adapters.js.map +1 -0
  86. package/dist/model3d-core/index.cjs +95 -0
  87. package/dist/model3d-core/index.d.ts +167 -0
  88. package/dist/model3d-core/index.d.ts.map +1 -0
  89. package/dist/model3d-core/index.js +158 -0
  90. package/dist/model3d-core/index.js.map +1 -0
  91. package/dist/notifications-core/index.cjs +315 -0
  92. package/dist/notifications-core/index.d.ts +229 -0
  93. package/dist/notifications-core/index.d.ts.map +1 -0
  94. package/dist/notifications-core/index.js +445 -0
  95. package/dist/notifications-core/index.js.map +1 -0
  96. package/dist/portal-core/index.d.ts +451 -0
  97. package/dist/portal-core/index.d.ts.map +1 -0
  98. package/dist/portal-core/index.js +819 -0
  99. package/dist/portal-core/index.js.map +1 -0
  100. package/dist/portal.d.ts +7 -0
  101. package/dist/portal.d.ts.map +1 -0
  102. package/dist/portal.js +7 -0
  103. package/dist/portal.js.map +1 -0
  104. package/dist/push-core/index.cjs +171 -0
  105. package/dist/push-core/index.d.ts +149 -0
  106. package/dist/push-core/index.d.ts.map +1 -0
  107. package/dist/push-core/index.js +231 -0
  108. package/dist/push-core/index.js.map +1 -0
  109. package/dist/react-marketing.d.ts +1 -17
  110. package/dist/react-marketing.d.ts.map +1 -1
  111. package/dist/react-marketing.js +4 -0
  112. package/dist/react-marketing.js.map +1 -1
  113. package/dist/react.d.ts +294 -101
  114. package/dist/react.d.ts.map +1 -1
  115. package/dist/react.js +302 -246
  116. package/dist/react.js.map +1 -1
  117. package/dist/seed-core/index.d.ts +354 -0
  118. package/dist/seed-core/index.d.ts.map +1 -0
  119. package/dist/seed-core/index.js +420 -0
  120. package/dist/seed-core/index.js.map +1 -0
  121. package/dist/seed.d.ts +7 -0
  122. package/dist/seed.d.ts.map +1 -0
  123. package/dist/seed.js +7 -0
  124. package/dist/seed.js.map +1 -0
  125. package/dist/seo-core/blog.d.ts +94 -0
  126. package/dist/seo-core/blog.d.ts.map +1 -0
  127. package/dist/seo-core/blog.js +149 -0
  128. package/dist/seo-core/blog.js.map +1 -0
  129. package/dist/seo-core/commerce.d.ts +64 -0
  130. package/dist/seo-core/commerce.d.ts.map +1 -0
  131. package/dist/seo-core/commerce.js +87 -0
  132. package/dist/seo-core/commerce.js.map +1 -0
  133. package/dist/seo-core/index.cjs +751 -0
  134. package/dist/seo-core/index.d.ts +226 -0
  135. package/dist/seo-core/index.d.ts.map +1 -0
  136. package/dist/seo-core/index.js +455 -0
  137. package/dist/seo-core/index.js.map +1 -0
  138. package/dist/seo-core/local.d.ts +181 -0
  139. package/dist/seo-core/local.d.ts.map +1 -0
  140. package/dist/seo-core/local.js +193 -0
  141. package/dist/seo-core/local.js.map +1 -0
  142. package/dist/sitemap-core/index.cjs +167 -0
  143. package/dist/sitemap-core/index.d.ts +155 -0
  144. package/dist/sitemap-core/index.d.ts.map +1 -0
  145. package/dist/sitemap-core/index.js +195 -0
  146. package/dist/sitemap-core/index.js.map +1 -0
  147. package/dist/social-core/index.d.ts +74 -0
  148. package/dist/social-core/index.d.ts.map +1 -0
  149. package/dist/social-core/index.js +72 -0
  150. package/dist/social-core/index.js.map +1 -0
  151. package/dist/social.d.ts +7 -0
  152. package/dist/social.d.ts.map +1 -0
  153. package/dist/social.js +7 -0
  154. package/dist/social.js.map +1 -0
  155. package/dist/solid-compat.d.ts +204 -0
  156. package/dist/solid-compat.d.ts.map +1 -0
  157. package/dist/solid-compat.js +221 -0
  158. package/dist/solid-compat.js.map +1 -0
  159. package/dist/store-core/index.d.ts +141 -0
  160. package/dist/store-core/index.d.ts.map +1 -0
  161. package/dist/store-core/index.js +277 -0
  162. package/dist/store-core/index.js.map +1 -0
  163. package/dist/store.d.ts +12 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +12 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/vanilla.d.ts +111 -0
  168. package/dist/vanilla.d.ts.map +1 -1
  169. package/dist/vanilla.js +206 -0
  170. package/dist/vanilla.js.map +1 -1
  171. package/dist/verify-core/index.d.ts +98 -0
  172. package/dist/verify-core/index.d.ts.map +1 -0
  173. package/dist/verify-core/index.js +186 -0
  174. package/dist/verify-core/index.js.map +1 -0
  175. package/dist/verify.d.ts +8 -0
  176. package/dist/verify.d.ts.map +1 -0
  177. package/dist/verify.js +8 -0
  178. package/dist/verify.js.map +1 -0
  179. package/openapi.json +4316 -1313
  180. package/package.json +121 -9
  181. package/src/account-core/index.ts +309 -0
  182. package/src/account.ts +11 -0
  183. package/src/angular/headless.ts +519 -0
  184. package/src/angular/index.ts +3 -0
  185. package/src/auth-links.ts +76 -0
  186. package/src/blog-core/index.ts +423 -0
  187. package/src/blog.ts +11 -0
  188. package/src/chat-core/index.ts +335 -0
  189. package/src/chat.ts +11 -0
  190. package/src/checkout-core/index.ts +225 -0
  191. package/src/checkout.ts +10 -0
  192. package/src/cli.ts +184 -28
  193. package/src/client.ts +998 -0
  194. package/src/contracts.ts +730 -11
  195. package/src/data-model-schema.ts +1 -1
  196. package/src/element.ts +102 -0
  197. package/src/embed-protocol.ts +100 -0
  198. package/src/framework-support.ts +440 -0
  199. package/src/legal-core/index.ts +276 -0
  200. package/src/marketing-core/index.ts +209 -0
  201. package/src/marketing.ts +7 -0
  202. package/src/mcp-adapters/index.ts +264 -0
  203. package/src/mcp-adapters.ts +7 -0
  204. package/src/model3d-core/index.ts +248 -0
  205. package/src/notifications-core/index.ts +650 -0
  206. package/src/portal-core/index.ts +1564 -0
  207. package/src/portal.ts +7 -0
  208. package/src/push-core/index.ts +335 -0
  209. package/src/qwik.tsx +602 -2
  210. package/src/react-marketing.tsx +5 -0
  211. package/src/react.tsx +434 -319
  212. package/src/seed-core/index.ts +735 -0
  213. package/src/seed.ts +7 -0
  214. package/src/seo-core/blog.ts +216 -0
  215. package/src/seo-core/commerce.ts +138 -0
  216. package/src/seo-core/index.ts +605 -0
  217. package/src/seo-core/local.ts +333 -0
  218. package/src/sitemap-core/index.ts +296 -0
  219. package/src/social-core/index.ts +103 -0
  220. package/src/social.ts +7 -0
  221. package/src/solid-compat.tsx +347 -0
  222. package/src/solid-surfaces.tsx +1311 -0
  223. package/src/solid.tsx +82 -60
  224. package/src/store-core/index.ts +426 -0
  225. package/src/store.ts +12 -0
  226. package/src/svelte/SocialLinks.svelte +84 -0
  227. package/src/svelte/headless.ts +435 -0
  228. package/src/svelte/index.ts +9 -0
  229. package/src/vanilla.ts +367 -0
  230. package/src/verify-core/index.ts +288 -0
  231. package/src/verify.ts +8 -0
  232. package/src/vue/headless.ts +527 -0
  233. package/src/vue/index.ts +3 -0
package/src/react.tsx CHANGED
@@ -1,3 +1,28 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Every export in this file is browser-only. The components hold state and
5
+ * subscribe to stores; the four programmatic helpers (`signIn`, `signUp`,
6
+ * `signOut`, `requestPasswordReset`) each bail out on
7
+ * `typeof window === "undefined"` because they navigate. Nothing here is meant
8
+ * to execute during a server render.
9
+ *
10
+ * WHY THE DIRECTIVE IS HERE
11
+ *
12
+ * Without it, a React Server Components framework treats this module as server
13
+ * code by default, and importing `<BiabForm>` into a page throws "you're
14
+ * importing a component that needs useState". The only way out is for every
15
+ * consumer to hand-write a `"use client"` wrapper per component — which is
16
+ * exactly what the Next App Router starter had to do, sixteen times, for
17
+ * components the SDK could have marked itself.
18
+ *
19
+ * `react-analytics.tsx` already carried this. `react.tsx` — the entry point
20
+ * that actually holds the components — did not.
21
+ *
22
+ * Bundlers that don't implement the directive treat it as a no-op string
23
+ * expression, so this is inert everywhere except RSC.
24
+ */
25
+
1
26
  import {
2
27
  type CSSProperties,
3
28
  createContext,
@@ -11,7 +36,22 @@ import {
11
36
  useState,
12
37
  useSyncExternalStore,
13
38
  } from "react";
14
-
39
+ import {
40
+ createFollowersController,
41
+ createUserController,
42
+ } from "./account-core/index.js";
43
+ import {
44
+ buildAuthHref,
45
+ clearLocalFollow,
46
+ DEFAULT_AUTH_BASE_PATH,
47
+ followerStoreKey,
48
+ type LocalFollow,
49
+ readLocalFollow,
50
+ writeLocalFollow,
51
+ } from "./auth-links.js";
52
+ import { createBlogController } from "./blog-core/index.js";
53
+ import { createChatController } from "./chat-core/index.js";
54
+ import { createCheckoutController } from "./checkout-core/index.js";
15
55
  import { BiabDevClient } from "./client.js";
16
56
  import type {
17
57
  BlogSessionInput,
@@ -37,6 +77,14 @@ import type {
37
77
  StorefrontSessionInput,
38
78
  StorefrontSessionResponse,
39
79
  } from "./contracts.js";
80
+ import {
81
+ type BiabEmbedEvent,
82
+ DEFAULT_EMBED_SANDBOX,
83
+ DEFAULT_REFRESH_LEAD_MS,
84
+ type EmbedSessionState,
85
+ refreshDelayMs,
86
+ subscribeToEmbedMessages,
87
+ } from "./embed-protocol.js";
40
88
  import {
41
89
  type AutocompleteConfig,
42
90
  type BiabFormsClient,
@@ -69,17 +117,25 @@ import {
69
117
  unitsForCategory,
70
118
  } from "./forms-core/units.js";
71
119
  import { US_STATES } from "./forms-core/us-states.js";
120
+ import { createMarketingController } from "./marketing-core/index.js";
121
+ import { createPortalController } from "./portal-core/index.js";
72
122
  import type { FormFileValue, FormSchema, FormSubmitResult } from "./sdk.js";
73
123
  import { createBiabClient } from "./sdk.js";
74
- import { resolveSocialProfiles } from "./socials.js";
124
+ import {
125
+ resolveSocialLinks,
126
+ SOCIAL_FALLBACK_GLYPH,
127
+ SOCIAL_LIST_STYLE,
128
+ } from "./social-core/index.js";
129
+ import { createStoreController } from "./store-core/index.js";
130
+ import {
131
+ type CreateVerifyControllerOptions,
132
+ createVerifyController,
133
+ } from "./verify-core/index.js";
75
134
 
76
135
  // ─── Shared session state machine ─────────────────────────────────────────
77
136
 
78
- type SessionState<T> =
79
- | { status: "idle" }
80
- | { status: "loading" }
81
- | { status: "ready"; session: T }
82
- | { status: "error"; error: Error };
137
+ /** Alias kept so the many call sites below read unchanged. */
138
+ type SessionState<T> = EmbedSessionState<T>;
83
139
 
84
140
  function useEmbedSession<T extends { url: string; expiresAt: string }>(
85
141
  loader: (client: BiabDevClient) => Promise<T>,
@@ -89,7 +145,7 @@ function useEmbedSession<T extends { url: string; expiresAt: string }>(
89
145
  refreshLeadMs?: number | undefined;
90
146
  },
91
147
  ) {
92
- const { apiKey, baseUrl, refreshLeadMs = 30_000 } = options;
148
+ const { apiKey, baseUrl, refreshLeadMs = DEFAULT_REFRESH_LEAD_MS } = options;
93
149
  const client = useMemo(
94
150
  () => new BiabDevClient({ apiKey, baseUrl }),
95
151
  [apiKey, baseUrl],
@@ -119,9 +175,15 @@ function useEmbedSession<T extends { url: string; expiresAt: string }>(
119
175
 
120
176
  useEffect(() => {
121
177
  if (state.status !== "ready") return;
122
- const expiresAt = Date.parse(state.session.expiresAt);
123
- if (Number.isNaN(expiresAt)) return;
124
- const delay = Math.max(1_000, expiresAt - Date.now() - refreshLeadMs);
178
+ // The schedule itself lives in `embed-protocol.ts` so both bindings
179
+ // re-mint at the same moment — an embed that expires in Solid but not in
180
+ // React would be a bug nobody thinks to look for.
181
+ const delay = refreshDelayMs(
182
+ state.session.expiresAt,
183
+ refreshLeadMs,
184
+ Date.now(),
185
+ );
186
+ if (delay === null) return;
125
187
  const timer = setTimeout(() => {
126
188
  void refresh();
127
189
  }, delay);
@@ -133,50 +195,23 @@ function useEmbedSession<T extends { url: string; expiresAt: string }>(
133
195
 
134
196
  // ─── postMessage protocol ─────────────────────────────────────────────────
135
197
 
136
- export type BiabEmbedEvent =
137
- | { type: "biab:ready"; surface: string }
138
- | { type: "biab:cart:update"; count: number; subtotalCents: number }
139
- | {
140
- type: "biab:cart:checkout-requested";
141
- lines: Array<{
142
- productId: string;
143
- variantId: string;
144
- combinationKey?: string;
145
- quantity: number;
146
- }>;
147
- }
148
- | {
149
- type: "biab:checkout:success";
150
- sessionId: string;
151
- orderId?: string | null;
152
- }
153
- | { type: "biab:checkout:cancel"; sessionId: string }
154
- | { type: "biab:checkout:error"; message: string }
155
- | { type: "biab:chatbot:user-message"; content: string }
156
- | { type: "biab:chatbot:assistant-message"; content: string; model?: string }
157
- | { type: "biab:chatbot:error"; message: string };
198
+ /**
199
+ * Re-exported from `./embed-protocol.js`, where the type and the origin check
200
+ * that guards it live together. Kept exported here so existing
201
+ * `@businessdash/sdk/react` imports keep working.
202
+ */
203
+ export type { BiabEmbedEvent } from "./embed-protocol.js";
158
204
 
159
205
  function useEmbedMessages(
160
206
  iframeRef: React.RefObject<HTMLIFrameElement | null>,
161
207
  handler: (event: BiabEmbedEvent) => void,
162
208
  ) {
163
- useEffect(() => {
164
- const onMessage = (e: MessageEvent) => {
165
- if (!e.data || typeof e.data !== "object") return;
166
- if ((e.data as { kind?: string }).kind !== "biab") return;
167
- if (
168
- iframeRef.current &&
169
- e.source !== null &&
170
- e.source !== iframeRef.current.contentWindow
171
- )
172
- return;
173
- const { kind, ...rest } = e.data as { kind: string } & BiabEmbedEvent;
174
- void kind;
175
- handler(rest as BiabEmbedEvent);
176
- };
177
- window.addEventListener("message", onMessage);
178
- return () => window.removeEventListener("message", onMessage);
179
- }, [handler, iframeRef]);
209
+ // The listener, and the origin check inside it, live in
210
+ // `embed-protocol.ts` so the Solid binding enforces exactly the same rule.
211
+ useEffect(
212
+ () => subscribeToEmbedMessages(() => iframeRef.current, handler),
213
+ [handler, iframeRef],
214
+ );
180
215
  }
181
216
 
182
217
  // ─── Dashboard (existing) ─────────────────────────────────────────────────
@@ -286,7 +321,7 @@ export function Dashboard({
286
321
  return (
287
322
  <iframe
288
323
  className={className}
289
- sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
324
+ sandbox={DEFAULT_EMBED_SANDBOX}
290
325
  src={state.session.url}
291
326
  style={{ ...defaultIframeStyle, ...style }}
292
327
  title={title}
@@ -353,9 +388,9 @@ function EmbedIframe<T extends { url: string }>({
353
388
  );
354
389
  }
355
390
 
356
- const sandbox = `allow-scripts allow-forms allow-same-origin allow-popups${
357
- extraSandbox ? ` ${extraSandbox}` : ""
358
- }`;
391
+ const sandbox = extraSandbox
392
+ ? `${DEFAULT_EMBED_SANDBOX} ${extraSandbox}`
393
+ : DEFAULT_EMBED_SANDBOX;
359
394
 
360
395
  return (
361
396
  <iframe
@@ -779,127 +814,100 @@ export function Chatbot({
779
814
  */
780
815
  export function useChatbot(options: { apiKey: string; baseUrl: string }) {
781
816
  const { apiKey, baseUrl } = options;
782
- const client = useMemo(
783
- () => new BiabDevClient({ apiKey, baseUrl }),
817
+ const controller = useMemo(
818
+ () =>
819
+ createChatController({
820
+ client: new BiabDevClient({ apiKey, baseUrl }),
821
+ }),
784
822
  [apiKey, baseUrl],
785
823
  );
786
- const [messages, setMessages] = useState<ChatbotMessage[]>([]);
787
- const [isPending, setIsPending] = useState(false);
788
- const [error, setError] = useState<Error | null>(null);
789
- const [latestUiActions, setLatestUiActions] = useState<ChatbotUiAction[]>([]);
790
- const [availability, setAvailability] = useState<ChatbotAvailability | null>(
791
- null,
792
- );
824
+ useEffect(() => () => controller.destroy(), [controller]);
793
825
 
794
- const send = useCallback(
795
- async (content: string): Promise<ChatbotChatResponse | null> => {
796
- const trimmed = content.trim();
797
- if (!trimmed) return null;
798
- setError(null);
799
- setIsPending(true);
800
- const next: ChatbotMessage[] = [
801
- ...messages,
802
- { role: "user", content: trimmed },
803
- ];
804
- setMessages(next);
805
- try {
806
- const input: ChatbotChatInput = { messages: next };
807
- const reply = await client.chatbot.chat(input);
808
- setMessages((prev) => [
809
- ...prev,
810
- { role: "assistant", content: reply.text },
811
- ]);
812
- setLatestUiActions(reply.uiActions ?? []);
813
- if (reply.availability) setAvailability(reply.availability);
814
- return reply;
815
- } catch (err) {
816
- const e = err instanceof Error ? err : new Error("Chat failed");
817
- setError(e);
818
- return null;
819
- } finally {
820
- setIsPending(false);
821
- }
822
- },
823
- [client, messages],
826
+ const subscribe = useCallback(
827
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
828
+ [controller],
824
829
  );
830
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
831
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
825
832
 
826
- const reset = useCallback(() => {
827
- setMessages([]);
828
- setError(null);
829
- setLatestUiActions([]);
830
- }, []);
831
-
832
- const getConfig = useCallback(
833
- async (): Promise<ChatbotConfig> => client.chatbot.getConfig(),
834
- [client],
833
+ const uploadFile = useCallback(
834
+ (args: { sessionId: string; file: File; uploaderEmail?: string | null }) =>
835
+ new BiabDevClient({ apiKey, baseUrl }).chatbot.uploadFile(args),
836
+ [apiKey, baseUrl],
835
837
  );
836
838
 
837
- const refreshAvailability =
838
- useCallback(async (): Promise<ChatbotAvailability | null> => {
839
- try {
840
- const next = await client.chatbot.getAvailability();
841
- setAvailability(next);
842
- return next;
843
- } catch {
844
- return null;
845
- }
846
- }, [client]);
839
+ // The returned shape is unchanged from before this hook was moved onto
840
+ // `chat-core`, so existing consumers keep working — only the implementation
841
+ // moved, and it now backs every other framework's chat binding too.
842
+ return {
843
+ messages: snap.messages,
844
+ send: controller.send,
845
+ reset: controller.reset,
846
+ isPending: snap.isPending,
847
+ error: snap.error,
848
+ getConfig: controller.loadConfig,
849
+ latestUiActions: snap.uiActions,
850
+ availability: snap.availability,
851
+ refreshAvailability: controller.refreshAvailability,
852
+ getForm: controller.getForm,
853
+ submitForm: controller.submitForm,
854
+ requestHuman: controller.requestHuman,
855
+ uploadFile,
856
+ };
857
+ }
847
858
 
848
- const getForm = useCallback(
849
- async (slug: string): Promise<ChatbotFormDefinition | null> => {
850
- try {
851
- // Chat `show_form` flow the chatbot path (Live + chatbotAccess).
852
- return await client.chatbot.getChatForm(slug);
853
- } catch {
854
- return null;
855
- }
856
- },
857
- [client],
859
+ /**
860
+ * Headless storefront + cart for React — the native alternative to
861
+ * `<Storefront>`, which is a hosted page in an iframe. Everything lives in
862
+ * `createStoreController`; this only bridges it into React.
863
+ *
864
+ * ```tsx
865
+ * const store = useStorefront({ apiKey, baseUrl });
866
+ * return store.products.map((p) => <MyCard key={p.id} product={p} />);
867
+ * ```
868
+ */
869
+ export function useStorefront(options: {
870
+ apiKey: string;
871
+ baseUrl: string;
872
+ pageSize?: number;
873
+ categoryId?: string | null;
874
+ }) {
875
+ const { apiKey, baseUrl, pageSize, categoryId } = options;
876
+ const controller = useMemo(
877
+ () =>
878
+ createStoreController({
879
+ client: new BiabDevClient({ apiKey, baseUrl }),
880
+ ...(pageSize ? { pageSize } : {}),
881
+ ...(categoryId ? { categoryId } : {}),
882
+ }),
883
+ [apiKey, baseUrl, pageSize, categoryId],
858
884
  );
859
885
 
860
- const submitForm = useCallback(
861
- async (
862
- slug: string,
863
- input: ChatbotFormSubmitInput,
864
- ): Promise<ChatbotFormSubmitResponse | null> => {
865
- try {
866
- const priorChat = messages.map((m, i) => ({
867
- role: m.role,
868
- content: m.content,
869
- createdAt: i,
870
- }));
871
- // Chatbot submit carries the transcript → chatbot path.
872
- return await client.chatbot.submitChatForm(slug, {
873
- ...input,
874
- priorChat: input.priorChat ?? priorChat,
875
- });
876
- } catch (err) {
877
- setError(err instanceof Error ? err : new Error("Submit failed"));
878
- return null;
879
- }
880
- },
881
- [client, messages],
882
- );
886
+ useEffect(() => {
887
+ void controller.load();
888
+ return () => controller.destroy();
889
+ }, [controller]);
883
890
 
884
- const uploadFile = useCallback(
885
- (args: { sessionId: string; file: File; uploaderEmail?: string | null }) =>
886
- client.chatbot.uploadFile(args),
887
- [client],
891
+ const subscribe = useCallback(
892
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
893
+ [controller],
888
894
  );
895
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
896
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
889
897
 
890
898
  return {
891
- messages,
892
- send,
893
- reset,
894
- isPending,
895
- error,
896
- getConfig,
897
- latestUiActions,
898
- availability,
899
- refreshAvailability,
900
- getForm,
901
- submitForm,
902
- uploadFile,
899
+ ...snap,
900
+ loadMore: controller.loadMore,
901
+ filterByCategory: controller.filterByCategory,
902
+ openProduct: controller.openProduct,
903
+ closeProduct: controller.closeProduct,
904
+ addToCart: controller.addToCart,
905
+ updateQuantity: controller.updateQuantity,
906
+ removeItem: controller.removeItem,
907
+ applyCoupon: controller.applyCoupon,
908
+ removeCoupon: controller.removeCoupon,
909
+ clearCart: controller.clearCart,
910
+ refreshCart: controller.refreshCart,
903
911
  };
904
912
  }
905
913
 
@@ -1323,7 +1331,10 @@ export function ChatbotInline({
1323
1331
  let cancelled = false;
1324
1332
  void getConfig()
1325
1333
  .then((cfg) => {
1326
- if (cancelled) return;
1334
+ // `null` means the config couldn't be read — the core reports that
1335
+ // rather than throwing, because a chat that can't fetch its upload
1336
+ // settings should still let the visitor talk. Keep the defaults.
1337
+ if (cancelled || !cfg) return;
1327
1338
  setFileUploadsEnabled(cfg.fileUploadsEnabled);
1328
1339
  setEmailCaptureMode(cfg.emailCaptureMode);
1329
1340
  })
@@ -2050,23 +2061,8 @@ type AuthLinkProps = {
2050
2061
  children?: ReactNode;
2051
2062
  };
2052
2063
 
2053
- const DEFAULT_AUTH_BASE_PATH = "/api/biab-auth";
2054
-
2055
- function buildAuthHref(
2056
- subroute: "sign-in" | "sign-up" | "sign-out",
2057
- props: {
2058
- returnTo?: string | undefined;
2059
- loginHint?: string | undefined;
2060
- basePath?: string | undefined;
2061
- },
2062
- ): string {
2063
- const base = (props.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(/\/$/, "");
2064
- const params = new URLSearchParams();
2065
- if (props.returnTo) params.set("returnTo", props.returnTo);
2066
- if (props.loginHint) params.set("loginHint", props.loginHint);
2067
- const qs = params.toString();
2068
- return `${base}/${subroute}${qs ? `?${qs}` : ""}`;
2069
- }
2064
+ // `buildAuthHref` and `DEFAULT_AUTH_BASE_PATH` come from `./auth-links.js` so
2065
+ // every binding builds the same handler URLs from the same query contract.
2070
2066
 
2071
2067
  export function SignIn({ children, ...rest }: AuthLinkProps) {
2072
2068
  return (
@@ -2233,37 +2229,29 @@ type UseUserState =
2233
2229
  * the cookie + handler path are the source of truth.
2234
2230
  */
2235
2231
  export function useUser(options?: { basePath?: string }): UseUserState {
2236
- const basePath = (options?.basePath ?? DEFAULT_AUTH_BASE_PATH).replace(
2237
- /\/$/,
2238
- "",
2232
+ const basePath = options?.basePath;
2233
+ const controller = useMemo(
2234
+ () => createUserController(basePath ? { basePath } : {}),
2235
+ [basePath],
2239
2236
  );
2240
- const [state, setState] = useState<UseUserState>({
2241
- status: "loading",
2242
- user: null,
2243
- });
2244
2237
 
2245
2238
  useEffect(() => {
2246
- let cancelled = false;
2247
- fetch(`${basePath}/me`, { credentials: "include" })
2248
- .then((r) => (r.ok ? r.json() : null))
2249
- .then((body: TenantAuthMeResponse) => {
2250
- if (cancelled) return;
2251
- if (body) {
2252
- setState({ status: "signed-in", user: body });
2253
- } else {
2254
- setState({ status: "signed-out", user: null });
2255
- }
2256
- })
2257
- .catch(() => {
2258
- if (cancelled) return;
2259
- setState({ status: "signed-out", user: null });
2260
- });
2261
- return () => {
2262
- cancelled = true;
2263
- };
2264
- }, [basePath]);
2239
+ controller.start();
2240
+ return () => controller.destroy();
2241
+ }, [controller]);
2265
2242
 
2266
- return state;
2243
+ const subscribe = useCallback(
2244
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
2245
+ [controller],
2246
+ );
2247
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
2248
+ // The shape is unchanged for callers; the fetch moved to `account-core` so
2249
+ // every framework reads the session the same way.
2250
+ return useSyncExternalStore(
2251
+ subscribe,
2252
+ getSnapshot,
2253
+ getSnapshot,
2254
+ ) as UseUserState;
2267
2255
  }
2268
2256
 
2269
2257
  // ─── Followers / subscribe ───────────────────────────────────────────────────
@@ -2278,39 +2266,9 @@ export type UseFollowersOptions = {
2278
2266
  baseUrl?: string;
2279
2267
  };
2280
2268
 
2281
- type LocalFollow = { email: string; at: number };
2282
-
2283
- const FOLLOWER_STORE_PREFIX = "biab.followers.";
2284
-
2285
- function readLocalFollow(key: string): LocalFollow | null {
2286
- if (typeof window === "undefined") return null;
2287
- try {
2288
- const raw = window.localStorage.getItem(key);
2289
- if (!raw) return null;
2290
- const parsed = JSON.parse(raw) as Partial<LocalFollow>;
2291
- return typeof parsed?.email === "string"
2292
- ? { email: parsed.email, at: Number(parsed.at) || 0 }
2293
- : null;
2294
- } catch {
2295
- return null;
2296
- }
2297
- }
2298
- function writeLocalFollow(key: string, value: LocalFollow): void {
2299
- if (typeof window === "undefined") return;
2300
- try {
2301
- window.localStorage.setItem(key, JSON.stringify(value));
2302
- } catch {
2303
- // storage disabled / quota — the local hint is best-effort.
2304
- }
2305
- }
2306
- function clearLocalFollow(key: string): void {
2307
- if (typeof window === "undefined") return;
2308
- try {
2309
- window.localStorage.removeItem(key);
2310
- } catch {
2311
- // ignore
2312
- }
2313
- }
2269
+ // `LocalFollow`, `followerStoreKey`, `readLocalFollow`, `writeLocalFollow` and
2270
+ // `clearLocalFollow` come from `./auth-links.js` — one storage format, so a
2271
+ // visitor who subscribed through one binding is recognised by another.
2314
2272
 
2315
2273
  /**
2316
2274
  * Visitor-facing followers/subscribe client, bound to a publishable token so it
@@ -2336,70 +2294,71 @@ function clearLocalFollow(key: string): void {
2336
2294
  */
2337
2295
  export function useFollowers(options: UseFollowersOptions) {
2338
2296
  const { token, siteId, baseUrl } = options;
2339
- const client = useMemo(
2297
+ const controller = useMemo(
2340
2298
  () =>
2341
- createBiabClient({
2342
- apiKey: token,
2299
+ createFollowersController({
2300
+ client: createBiabClient({
2301
+ apiKey: token,
2302
+ siteId,
2303
+ baseUrl: baseUrl ?? DEFAULT_PACKAGE_BASE_URL,
2304
+ }),
2343
2305
  siteId,
2344
- baseUrl: baseUrl ?? DEFAULT_PACKAGE_BASE_URL,
2345
2306
  }),
2346
2307
  [token, siteId, baseUrl],
2347
2308
  );
2348
- const storeKey = `${FOLLOWER_STORE_PREFIX}${siteId}`;
2349
- const [local, setLocal] = useState<LocalFollow | null>(() =>
2350
- readLocalFollow(storeKey),
2351
- );
2352
- useEffect(() => {
2353
- setLocal(readLocalFollow(storeKey));
2354
- }, [storeKey]);
2355
-
2356
- const join = useCallback(
2357
- async (input: { email: string; name?: string; source?: string }) => {
2358
- const res = await client.followers.join(input);
2359
- const entry: LocalFollow = { email: input.email, at: Date.now() };
2360
- writeLocalFollow(storeKey, entry);
2361
- setLocal(entry);
2362
- return res;
2363
- },
2364
- [client, storeKey],
2365
- );
2366
2309
 
2367
- const leave = useCallback(
2368
- async (email: string) => {
2369
- const res = await client.followers.leave({ email });
2370
- clearLocalFollow(storeKey);
2371
- setLocal(null);
2372
- return res;
2373
- },
2374
- [client, storeKey],
2375
- );
2310
+ useEffect(() => () => controller.destroy(), [controller]);
2376
2311
 
2377
- const me = useCallback(
2378
- (email: string) => client.followers.me({ email }),
2379
- [client],
2312
+ const subscribe = useCallback(
2313
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
2314
+ [controller],
2380
2315
  );
2316
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
2317
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2318
+
2319
+ // Same shape as before the move onto `account-core`, including the
2320
+ // local-storage hint, so existing consumers are untouched.
2321
+ return {
2322
+ join: controller.join,
2323
+ leave: controller.leave,
2324
+ me: controller.me,
2325
+ edit: controller.edit,
2326
+ subscribedLocally: snap.subscribedLocally,
2327
+ localEmail: snap.localEmail,
2328
+ };
2329
+ }
2381
2330
 
2382
- const edit = useCallback(
2383
- (email: string, patch: { name?: string; source?: string }) =>
2384
- client.followers.edit({ email, patch }),
2385
- [client],
2331
+ /**
2332
+ * Headless checkout for React. `start()` mints a Stripe session and `redirect()`
2333
+ * navigates — split apart so a native app or an SSR render can use the first
2334
+ * without the second.
2335
+ */
2336
+ export function useCheckout(options: { apiKey: string; baseUrl: string }) {
2337
+ const { apiKey, baseUrl } = options;
2338
+ const controller = useMemo(
2339
+ () =>
2340
+ createCheckoutController({
2341
+ client: new BiabDevClient({ apiKey, baseUrl }),
2342
+ }),
2343
+ [apiKey, baseUrl],
2386
2344
  );
2387
2345
 
2388
- return useMemo(
2389
- () => ({
2390
- join,
2391
- leave,
2392
- me,
2393
- edit,
2394
- /** This browser already subscribed (instant, no network). Use to hide a
2395
- * newsletter form for anonymous visitors. */
2396
- subscribedLocally: local !== null,
2397
- /** Email this browser subscribed with — pass to `me()` for an
2398
- * authoritative re-check, or `null` if this browser hasn't subscribed. */
2399
- localEmail: local?.email ?? null,
2400
- }),
2401
- [join, leave, me, edit, local],
2346
+ useEffect(() => () => controller.destroy(), [controller]);
2347
+
2348
+ const subscribe = useCallback(
2349
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
2350
+ [controller],
2402
2351
  );
2352
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
2353
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2354
+
2355
+ return {
2356
+ ...snap,
2357
+ start: controller.start,
2358
+ redirect: controller.redirect,
2359
+ resolve: controller.resolve,
2360
+ reset: controller.reset,
2361
+ };
2403
2362
  }
2404
2363
 
2405
2364
  // ─── Marketing pages — list, lazy-load per page, preload ─────────────────────
@@ -5380,22 +5339,17 @@ export function BiabForm(props: BiabFormProps) {
5380
5339
  }
5381
5340
 
5382
5341
  function SocialGlyph({
5383
- iconSlug,
5384
- color,
5342
+ iconUrl,
5385
5343
  size,
5386
5344
  className,
5387
5345
  label,
5388
5346
  }: {
5389
- iconSlug: string | null;
5390
- color?: string | undefined;
5347
+ iconUrl: string | null;
5391
5348
  size: number;
5392
5349
  className?: string | undefined;
5393
5350
  label: string;
5394
5351
  }) {
5395
- if (iconSlug) {
5396
- const src = color
5397
- ? `https://cdn.simpleicons.org/${iconSlug}/${encodeURIComponent(color)}`
5398
- : `https://cdn.simpleicons.org/${iconSlug}`;
5352
+ if (iconUrl) {
5399
5353
  return (
5400
5354
  // biome-ignore lint: remote brand icon from the simple-icons CDN
5401
5355
  <img
@@ -5403,7 +5357,7 @@ function SocialGlyph({
5403
5357
  className={className}
5404
5358
  height={size}
5405
5359
  loading="lazy"
5406
- src={src}
5360
+ src={iconUrl}
5407
5361
  width={size}
5408
5362
  />
5409
5363
  );
@@ -5415,13 +5369,13 @@ function SocialGlyph({
5415
5369
  fill="none"
5416
5370
  height={size}
5417
5371
  stroke="currentColor"
5418
- strokeWidth={1.8}
5419
- viewBox="0 0 24 24"
5372
+ strokeWidth={SOCIAL_FALLBACK_GLYPH.strokeWidth}
5373
+ viewBox={SOCIAL_FALLBACK_GLYPH.viewBox}
5420
5374
  width={size}
5421
5375
  >
5422
5376
  <title>{label}</title>
5423
- <circle cx="12" cy="12" r="9" />
5424
- <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />
5377
+ <circle {...SOCIAL_FALLBACK_GLYPH.circle} />
5378
+ <path d={SOCIAL_FALLBACK_GLYPH.path} />
5425
5379
  </svg>
5426
5380
  );
5427
5381
  }
@@ -5455,21 +5409,12 @@ export function SocialLinks({
5455
5409
  linkClassName,
5456
5410
  iconClassName,
5457
5411
  }: SocialLinksProps) {
5458
- const rows = resolveSocialProfiles(socials);
5412
+ // Rows, URLs and icon URLs come from `social-core`, shared with every other
5413
+ // binding — only the markup below is React's.
5414
+ const rows = resolveSocialLinks(socials, { color });
5459
5415
  if (rows.length === 0) return null;
5460
5416
  return (
5461
- <ul
5462
- className={className}
5463
- style={{
5464
- display: "flex",
5465
- flexWrap: "wrap",
5466
- alignItems: "center",
5467
- gap: "0.75rem",
5468
- listStyle: "none",
5469
- margin: 0,
5470
- padding: 0,
5471
- }}
5472
- >
5417
+ <ul className={className} style={SOCIAL_LIST_STYLE as CSSProperties}>
5473
5418
  {rows.map((row) => (
5474
5419
  <li key={row.key}>
5475
5420
  <a
@@ -5482,8 +5427,7 @@ export function SocialLinks({
5482
5427
  >
5483
5428
  <SocialGlyph
5484
5429
  className={iconClassName}
5485
- color={color}
5486
- iconSlug={row.iconSlug}
5430
+ iconUrl={row.iconUrl}
5487
5431
  label={row.label}
5488
5432
  size={size}
5489
5433
  />
@@ -5493,3 +5437,174 @@ export function SocialLinks({
5493
5437
  </ul>
5494
5438
  );
5495
5439
  }
5440
+
5441
+ /**
5442
+ * Headless blog for React — the native alternative to `<Blog>`, which is a
5443
+ * hosted page in an iframe. Posts, taxonomy, a single post, comments and likes
5444
+ * as data; the layout and the URLs are yours.
5445
+ *
5446
+ * ```tsx
5447
+ * const blog = useBlog({ apiKey, baseUrl });
5448
+ * return blog.posts.map((p) => <MyCard key={p.slug} post={p} />);
5449
+ * ```
5450
+ */
5451
+ export function useBlog(options: {
5452
+ apiKey: string;
5453
+ baseUrl: string;
5454
+ pageSize?: number;
5455
+ categoryId?: string | null;
5456
+ }) {
5457
+ const { apiKey, baseUrl, pageSize, categoryId } = options;
5458
+ const controller = useMemo(
5459
+ () =>
5460
+ createBlogController({
5461
+ client: new BiabDevClient({ apiKey, baseUrl }),
5462
+ ...(pageSize ? { pageSize } : {}),
5463
+ ...(categoryId ? { categoryId } : {}),
5464
+ }),
5465
+ [apiKey, baseUrl, pageSize, categoryId],
5466
+ );
5467
+
5468
+ useEffect(() => {
5469
+ void controller.load();
5470
+ return () => controller.destroy();
5471
+ }, [controller]);
5472
+
5473
+ const subscribe = useCallback(
5474
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
5475
+ [controller],
5476
+ );
5477
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
5478
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5479
+
5480
+ return {
5481
+ ...snap,
5482
+ loadMore: controller.loadMore,
5483
+ filterByCategory: controller.filterByCategory,
5484
+ openPost: controller.openPost,
5485
+ closePost: controller.closePost,
5486
+ loadComments: controller.loadComments,
5487
+ loadMoreComments: controller.loadMoreComments,
5488
+ postComment: controller.postComment,
5489
+ toggleLike: controller.toggleLike,
5490
+ };
5491
+ }
5492
+
5493
+ /**
5494
+ * Headless email / phone verification for React.
5495
+ *
5496
+ * Read `flow` before rendering: `otp` wants a code field, `link` wants a
5497
+ * "check your email" message and no field at all.
5498
+ */
5499
+ export function useVerify(
5500
+ options: { apiKey: string; baseUrl: string } & Omit<
5501
+ CreateVerifyControllerOptions,
5502
+ "client"
5503
+ >,
5504
+ ) {
5505
+ const { apiKey, baseUrl, kind, resendCooldownSeconds } = options;
5506
+ const controller = useMemo(
5507
+ () =>
5508
+ createVerifyController({
5509
+ client: new BiabDevClient({ apiKey, baseUrl }),
5510
+ kind,
5511
+ ...(resendCooldownSeconds ? { resendCooldownSeconds } : {}),
5512
+ }),
5513
+ [apiKey, baseUrl, kind, resendCooldownSeconds],
5514
+ );
5515
+
5516
+ useEffect(() => () => controller.destroy(), [controller]);
5517
+
5518
+ const subscribe = useCallback(
5519
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
5520
+ [controller],
5521
+ );
5522
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
5523
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5524
+
5525
+ return {
5526
+ ...snap,
5527
+ start: controller.start,
5528
+ resend: controller.resend,
5529
+ confirm: controller.confirm,
5530
+ canResend: controller.canResend,
5531
+ reset: controller.reset,
5532
+ };
5533
+ }
5534
+
5535
+ /**
5536
+ * The signed-in customer's portal for React — work, quotes, contracts,
5537
+ * invoices, orders, downloads, shipping, reviews, referrals, profile and
5538
+ * notification settings. The native alternative to `<Dashboard>`, which is a
5539
+ * hosted page in an iframe.
5540
+ *
5541
+ * `sessionToken` is required: every portal route is scoped to the signed-in
5542
+ * customer, and without one the portal renders empty rather than signed-out.
5543
+ *
5544
+ * Two things the API imposes and this hook does not hide. There are no list
5545
+ * endpoints for invoices, quotes, contracts or shipments — `work` carries the
5546
+ * counts and you open each by id. And a submitted review is queued for staff
5547
+ * approval, so it will NOT appear in `reviews`; watch `reviewAwaitingModeration`
5548
+ * to tell the customer it is pending rather than live.
5549
+ */
5550
+ export function usePortal(options: {
5551
+ apiKey: string;
5552
+ baseUrl: string;
5553
+ sessionToken: string;
5554
+ }) {
5555
+ const { apiKey, baseUrl, sessionToken } = options;
5556
+ const controller = useMemo(
5557
+ () =>
5558
+ createPortalController({
5559
+ client: new BiabDevClient({ apiKey, baseUrl }),
5560
+ sessionToken,
5561
+ }),
5562
+ [apiKey, baseUrl, sessionToken],
5563
+ );
5564
+
5565
+ useEffect(() => {
5566
+ void controller.load();
5567
+ return () => controller.destroy();
5568
+ }, [controller]);
5569
+
5570
+ const subscribe = useCallback(
5571
+ (onStoreChange: () => void) => controller.subscribe(() => onStoreChange()),
5572
+ [controller],
5573
+ );
5574
+ const getSnapshot = useCallback(() => controller.snapshot(), [controller]);
5575
+ const snap = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5576
+
5577
+ return {
5578
+ ...snap,
5579
+ loadOrders: controller.loadOrders,
5580
+ loadInvoices: controller.loadInvoices,
5581
+ loadQuotes: controller.loadQuotes,
5582
+ loadContracts: controller.loadContracts,
5583
+ loadShipments: controller.loadShipments,
5584
+ loadMessages: controller.loadMessages,
5585
+ postMessage: controller.postMessage,
5586
+ markMessagesRead: controller.markMessagesRead,
5587
+ loadPayments: controller.loadPayments,
5588
+ loadReviews: controller.loadReviews,
5589
+ loadReferrals: controller.loadReferrals,
5590
+ loadNotificationPreferences: controller.loadNotificationPreferences,
5591
+ loadOtherOrgs: controller.loadOtherOrgs,
5592
+ openJob: controller.openJob,
5593
+ openOrder: controller.openOrder,
5594
+ openInvoice: controller.openInvoice,
5595
+ openQuote: controller.openQuote,
5596
+ openContract: controller.openContract,
5597
+ trackShipment: controller.trackShipment,
5598
+ closeDetail: controller.closeDetail,
5599
+ postJobComment: controller.postJobComment,
5600
+ acceptQuote: controller.acceptQuote,
5601
+ rejectQuote: controller.rejectQuote,
5602
+ signContract: controller.signContract,
5603
+ payInvoice: controller.payInvoice,
5604
+ updateProfile: controller.updateProfile,
5605
+ updateNotificationPreferences: controller.updateNotificationPreferences,
5606
+ submitReview: controller.submitReview,
5607
+ submitReturnRequest: controller.submitReturnRequest,
5608
+ contactCompany: controller.contactCompany,
5609
+ };
5610
+ }