@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,1311 @@
1
+ /**
2
+ * `@businessdash/sdk/solid` — the product surfaces beyond forms.
3
+ *
4
+ * Solid used to stop at `<BiabForm>` while React carried the whole product:
5
+ * storefront, cart, checkout, blog, the customer portal, chat, auth, followers
6
+ * and social links. This file closes most of that gap.
7
+ *
8
+ * ## Why this was cheap, and what it means
9
+ *
10
+ * Six of these surfaces are not bespoke UI. `Storefront`, `Cart`, `Checkout`,
11
+ * `Blog`, `Chatbot` and `Dashboard` are BusinessDash-hosted pages rendered in a
12
+ * sandboxed iframe: the binding mints a short-lived session, points an iframe
13
+ * at its URL, and relays `postMessage` events back to the host. That logic is
14
+ * two primitives — `createEmbedSession` and `<EmbedIframe>` — and every embed
15
+ * surface is a dozen lines on top of them. The React binding is built the same
16
+ * way; this is the same architecture expressed in signals.
17
+ *
18
+ * The consequence worth stating plainly: these surfaces render BusinessDash's
19
+ * markup, not yours. They are the fastest path to a working store, not a
20
+ * design-system component. `<BiabForm>` remains the only surface here that
21
+ * renders into your own DOM, and `BiabClient` is always available if you want
22
+ * to build the UI yourself.
23
+ *
24
+ * ## Solid specifics
25
+ *
26
+ * Props are never destructured — a Solid props object is a reactive proxy and
27
+ * pulling values out of it at call time freezes them. Everything reads
28
+ * `props.x` at the point of use. External state (session status, auth, the
29
+ * follower hint) lives in signals, and every listener and timer is torn down
30
+ * through `onCleanup`.
31
+ *
32
+ * Ship mode: SOURCE. Compiled by the consumer's Solid toolchain, never by tsc
33
+ * here — same as `solid.tsx`.
34
+ *
35
+ * @module
36
+ */
37
+
38
+ // Only primitives that exist UNCHANGED in both Solid 1 and Solid 2. Everything
39
+ // Solid 2 moved or dropped comes from `solid-compat` — see that module for the
40
+ // measured differences.
41
+ import {
42
+ createContext,
43
+ createEffect,
44
+ createMemo,
45
+ createSignal,
46
+ onCleanup,
47
+ useContext,
48
+ } from "solid-js";
49
+ import {
50
+ createFollowersController,
51
+ createUserController,
52
+ } from "./account-core/index.js";
53
+ // Only the href builder is still needed here; the follower storage helpers and
54
+ // the session read moved to `account-core` when this binding stopped
55
+ // hand-rolling them.
56
+ import { buildAuthHref } from "./auth-links.js";
57
+ import { createBlogController } from "./blog-core/index.js";
58
+ import { createChatController } from "./chat-core/index.js";
59
+ import { createCheckoutController } from "./checkout-core/index.js";
60
+ import { BiabDevClient } from "./client.js";
61
+ import type {
62
+ BlogCommentPostInput,
63
+ BlogSessionInput,
64
+ BlogSessionResponse,
65
+ CartAddItemInput,
66
+ CartSessionResponse,
67
+ ChatbotFormSubmitInput,
68
+ ChatbotRequestHumanInput,
69
+ ChatbotSessionResponse,
70
+ CheckoutSessionInput,
71
+ CheckoutSessionResponse,
72
+ CustomerContactCompanyInput,
73
+ CustomerInvoicePayInput,
74
+ CustomerJobCommentPostInput,
75
+ CustomerPortalMessagePostInput,
76
+ CustomerPortalUpdateProfileInput,
77
+ CustomerReturnRequestInput,
78
+ CustomerReviewSubmitInput,
79
+ DashboardSessionResponse,
80
+ NotificationPreferencesUpdateInput,
81
+ StorefrontSessionInput,
82
+ StorefrontSessionResponse,
83
+ } from "./contracts.js";
84
+ import {
85
+ type BiabEmbedEvent,
86
+ DEFAULT_EMBED_SANDBOX,
87
+ DEFAULT_REFRESH_LEAD_MS,
88
+ type EmbedSessionState,
89
+ refreshDelayMs,
90
+ subscribeToEmbedMessages,
91
+ } from "./embed-protocol.js";
92
+ import { createMarketingController } from "./marketing-core/index.js";
93
+ import { createPortalController } from "./portal-core/index.js";
94
+ import { createBiabClient } from "./sdk.js";
95
+ import {
96
+ resolveSocialLinks,
97
+ SOCIAL_FALLBACK_GLYPH,
98
+ SOCIAL_LIST_STYLE,
99
+ } from "./social-core/index.js";
100
+ import {
101
+ For,
102
+ onMount,
103
+ providerFor,
104
+ renderNode,
105
+ Show,
106
+ type SolidNode,
107
+ type SolidStyle,
108
+ } from "./solid-compat.js";
109
+ import { createStoreController } from "./store-core/index.js";
110
+ import {
111
+ type CreateVerifyControllerOptions,
112
+ createVerifyController,
113
+ } from "./verify-core/index.js";
114
+
115
+ export type { BiabEmbedEvent } from "./embed-protocol.js";
116
+
117
+ /** Package-API base URL used when a surface doesn't specify one. */
118
+ export const DEFAULT_PACKAGE_BASE_URL =
119
+ "https://businessdash.us/api/package/v1";
120
+
121
+ const defaultIframeStyle: SolidStyle = {
122
+ width: "100%",
123
+ height: "100%",
124
+ "min-height": "600px",
125
+ border: "0",
126
+ display: "block",
127
+ };
128
+
129
+ const messageStyle: SolidStyle = { padding: "16px" };
130
+
131
+ // ─────────────────────────────────────────────────────────────────────────────
132
+ // createEmbedSession — mint a session, keep it fresh
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+
135
+ export type EmbedSessionOptions = {
136
+ apiKey: string;
137
+ baseUrl: string;
138
+ /** Refresh this many ms before the session expires. Default 30s. */
139
+ refreshLeadMs?: number | undefined;
140
+ };
141
+
142
+ /**
143
+ * Create and maintain an embed session.
144
+ *
145
+ * `options` is a getter so the session follows a changing `apiKey`/`baseUrl`
146
+ * instead of capturing whatever they were on first render — the Solid
147
+ * equivalent of React's dependency array, and the reason props aren't
148
+ * destructured on the way in.
149
+ *
150
+ * The session is re-minted shortly before it expires, so a page left open
151
+ * overnight doesn't wake up to a dead iframe.
152
+ */
153
+ export function createEmbedSession<
154
+ T extends { url: string; expiresAt: string },
155
+ >(
156
+ loader: (client: BiabDevClient) => Promise<T>,
157
+ options: () => EmbedSessionOptions,
158
+ ) {
159
+ const client = createMemo(
160
+ () =>
161
+ new BiabDevClient({
162
+ apiKey: options().apiKey,
163
+ baseUrl: options().baseUrl,
164
+ }),
165
+ );
166
+
167
+ const [state, setState] = createSignal<EmbedSessionState<T>>({
168
+ status: "idle",
169
+ });
170
+
171
+ const refresh = async (): Promise<void> => {
172
+ setState({ status: "loading" });
173
+ try {
174
+ const session = await loader(client());
175
+ setState({ status: "ready", session });
176
+ } catch (error) {
177
+ setState({
178
+ status: "error",
179
+ error:
180
+ error instanceof Error ? error : new Error("Unknown session error"),
181
+ });
182
+ }
183
+ };
184
+
185
+ // Mint on mount, and again whenever the client identity changes.
186
+ createEffect(() => {
187
+ client();
188
+ void refresh();
189
+ });
190
+
191
+ // Re-mint before expiry. Re-runs on every status change, which is what
192
+ // schedules the next timer after each successful refresh.
193
+ createEffect(() => {
194
+ const current = state();
195
+ if (current.status !== "ready") return;
196
+ const delay = refreshDelayMs(
197
+ current.session.expiresAt,
198
+ options().refreshLeadMs ?? DEFAULT_REFRESH_LEAD_MS,
199
+ Date.now(),
200
+ );
201
+ if (delay === null) return;
202
+ const timer = setTimeout(() => void refresh(), delay);
203
+ onCleanup(() => clearTimeout(timer));
204
+ });
205
+
206
+ return { client, state, refresh };
207
+ }
208
+
209
+ // ─────────────────────────────────────────────────────────────────────────────
210
+ // EmbedIframe — the shared shell for every hosted surface
211
+ // ─────────────────────────────────────────────────────────────────────────────
212
+
213
+ type EmbedIframeProps<T extends { url: string }> = {
214
+ state: EmbedSessionState<T>;
215
+ refresh: () => Promise<void>;
216
+ title: string;
217
+ class?: string | undefined;
218
+ style?: SolidStyle | undefined;
219
+ loadingFallback?: SolidNode | undefined;
220
+ errorFallback?:
221
+ | ((error: Error, refresh: () => void) => SolidNode)
222
+ | undefined;
223
+ onEvent?: ((event: BiabEmbedEvent) => void) | undefined;
224
+ /** Extra `sandbox` tokens beyond the safe defaults. */
225
+ extraSandbox?: string | undefined;
226
+ };
227
+
228
+ function EmbedIframe<T extends { url: string }>(props: EmbedIframeProps<T>) {
229
+ let iframe: HTMLIFrameElement | undefined;
230
+
231
+ onMount(() => {
232
+ // The handler is read through `props` on every message rather than
233
+ // captured, so a host swapping its callback takes effect immediately.
234
+ const unsubscribe = subscribeToEmbedMessages(
235
+ () => iframe,
236
+ (event) => props.onEvent?.(event),
237
+ );
238
+ onCleanup(unsubscribe);
239
+ });
240
+
241
+ const errored = () =>
242
+ props.state.status === "error" ? props.state.error : null;
243
+ const ready = () =>
244
+ props.state.status === "ready" ? props.state.session : null;
245
+
246
+ return (
247
+ <Show
248
+ fallback={
249
+ props.errorFallback ? (
250
+ props.errorFallback(errored() as Error, () => void props.refresh())
251
+ ) : (
252
+ <div class={props.class} style={{ ...messageStyle, ...props.style }}>
253
+ Embed unavailable: {errored()?.message}
254
+ </div>
255
+ )
256
+ }
257
+ when={!errored()}
258
+ >
259
+ <Show
260
+ fallback={
261
+ props.loadingFallback ?? (
262
+ <div
263
+ class={props.class}
264
+ style={{ ...messageStyle, ...props.style }}
265
+ >
266
+ Loading…
267
+ </div>
268
+ )
269
+ }
270
+ when={ready()}
271
+ >
272
+ {(session) => (
273
+ <iframe
274
+ class={props.class}
275
+ ref={iframe}
276
+ sandbox={
277
+ props.extraSandbox
278
+ ? `${DEFAULT_EMBED_SANDBOX} ${props.extraSandbox}`
279
+ : DEFAULT_EMBED_SANDBOX
280
+ }
281
+ src={session().url}
282
+ style={{ ...defaultIframeStyle, ...props.style }}
283
+ title={props.title}
284
+ />
285
+ )}
286
+ </Show>
287
+ </Show>
288
+ );
289
+ }
290
+
291
+ /** Props every hosted embed surface accepts. */
292
+ type BaseEmbedProps = {
293
+ apiKey: string;
294
+ baseUrl: string;
295
+ class?: string;
296
+ style?: SolidStyle;
297
+ title?: string;
298
+ loadingFallback?: SolidNode;
299
+ errorFallback?: (error: Error, refresh: () => void) => SolidNode;
300
+ onEvent?: (event: BiabEmbedEvent) => void;
301
+ refreshLeadMs?: number;
302
+ };
303
+
304
+ // ─── Storefront ──────────────────────────────────────────────────────────────
305
+
306
+ export type StorefrontProps = BaseEmbedProps & {
307
+ productId?: string;
308
+ /** Convenience: fires only on cart updates. */
309
+ onCartChange?: (count: number, subtotalCents: number) => void;
310
+ };
311
+
312
+ /** The org's hosted storefront, embedded. */
313
+ export function Storefront(props: StorefrontProps) {
314
+ const { state, refresh } = createEmbedSession<StorefrontSessionResponse>(
315
+ (client) => {
316
+ const input: StorefrontSessionInput = props.productId
317
+ ? { productId: props.productId }
318
+ : undefined;
319
+ return client.storefront.createSession(input);
320
+ },
321
+ () => ({
322
+ apiKey: props.apiKey,
323
+ baseUrl: props.baseUrl,
324
+ refreshLeadMs: props.refreshLeadMs,
325
+ }),
326
+ );
327
+
328
+ return (
329
+ <EmbedIframe
330
+ class={props.class}
331
+ errorFallback={props.errorFallback}
332
+ loadingFallback={props.loadingFallback}
333
+ onEvent={(event) => {
334
+ props.onEvent?.(event);
335
+ if (event.type === "biab:cart:update") {
336
+ props.onCartChange?.(event.count, event.subtotalCents);
337
+ }
338
+ }}
339
+ refresh={refresh}
340
+ state={state()}
341
+ style={props.style}
342
+ title={props.title ?? "Storefront"}
343
+ />
344
+ );
345
+ }
346
+
347
+ // ─── Cart ────────────────────────────────────────────────────────────────────
348
+
349
+ export type CartProps = BaseEmbedProps & {
350
+ onCartChange?: (count: number, subtotalCents: number) => void;
351
+ };
352
+
353
+ /** The visitor's cart, embedded. */
354
+ export function Cart(props: CartProps) {
355
+ const { state, refresh } = createEmbedSession<CartSessionResponse>(
356
+ (client) => client.cart.createSession(),
357
+ () => ({
358
+ apiKey: props.apiKey,
359
+ baseUrl: props.baseUrl,
360
+ refreshLeadMs: props.refreshLeadMs,
361
+ }),
362
+ );
363
+
364
+ return (
365
+ <EmbedIframe
366
+ class={props.class}
367
+ errorFallback={props.errorFallback}
368
+ loadingFallback={props.loadingFallback}
369
+ onEvent={(event) => {
370
+ props.onEvent?.(event);
371
+ if (event.type === "biab:cart:update") {
372
+ props.onCartChange?.(event.count, event.subtotalCents);
373
+ }
374
+ }}
375
+ refresh={refresh}
376
+ state={state()}
377
+ style={props.style}
378
+ title={props.title ?? "Cart"}
379
+ />
380
+ );
381
+ }
382
+
383
+ // ─── Blog ────────────────────────────────────────────────────────────────────
384
+
385
+ export type BlogProps = BaseEmbedProps & { postSlug?: string };
386
+
387
+ /** The org's blog — index, or a single post when `postSlug` is given. */
388
+ export function Blog(props: BlogProps) {
389
+ const { state, refresh } = createEmbedSession<BlogSessionResponse>(
390
+ (client) => {
391
+ const input: BlogSessionInput = props.postSlug
392
+ ? { postSlug: props.postSlug }
393
+ : undefined;
394
+ return client.blog.createSession(input);
395
+ },
396
+ () => ({
397
+ apiKey: props.apiKey,
398
+ baseUrl: props.baseUrl,
399
+ refreshLeadMs: props.refreshLeadMs,
400
+ }),
401
+ );
402
+
403
+ return (
404
+ <EmbedIframe
405
+ class={props.class}
406
+ errorFallback={props.errorFallback}
407
+ loadingFallback={props.loadingFallback}
408
+ onEvent={props.onEvent}
409
+ refresh={refresh}
410
+ state={state()}
411
+ style={props.style}
412
+ title={props.title ?? "Blog"}
413
+ />
414
+ );
415
+ }
416
+
417
+ // ─── Chatbot ─────────────────────────────────────────────────────────────────
418
+
419
+ export type ChatbotProps = BaseEmbedProps & {
420
+ /** Convenience: fires when the assistant replies. */
421
+ onAssistantMessage?: (content: string, model?: string) => void;
422
+ };
423
+
424
+ /**
425
+ * The org's Front Desk assistant, embedded.
426
+ *
427
+ * This is the hosted chat UI. React additionally ships a headless
428
+ * `useChatbot` + `<ChatbotInline>` for hosts that render their own chat; that
429
+ * pair has no Solid equivalent yet, and `BiabClient.chatbot` is the way to
430
+ * build one in the meantime.
431
+ */
432
+ export function Chatbot(props: ChatbotProps) {
433
+ const { state, refresh } = createEmbedSession<ChatbotSessionResponse>(
434
+ (client) => client.chatbot.createSession(),
435
+ () => ({
436
+ apiKey: props.apiKey,
437
+ baseUrl: props.baseUrl,
438
+ refreshLeadMs: props.refreshLeadMs,
439
+ }),
440
+ );
441
+
442
+ return (
443
+ <EmbedIframe
444
+ class={props.class}
445
+ errorFallback={props.errorFallback}
446
+ loadingFallback={props.loadingFallback}
447
+ onEvent={(event) => {
448
+ props.onEvent?.(event);
449
+ if (event.type === "biab:chatbot:assistant-message") {
450
+ props.onAssistantMessage?.(event.content, event.model);
451
+ }
452
+ }}
453
+ refresh={refresh}
454
+ state={state()}
455
+ style={props.style}
456
+ title={props.title ?? "Assistant"}
457
+ />
458
+ );
459
+ }
460
+
461
+ // ─── Checkout ────────────────────────────────────────────────────────────────
462
+
463
+ export type CheckoutProps = {
464
+ apiKey: string;
465
+ baseUrl: string;
466
+ cart: CheckoutSessionInput;
467
+ /**
468
+ * `"redirect"` (default) renders a button that sends the browser to Stripe
469
+ * at the top level. `"iframe"` embeds the checkout surface, where Stripe
470
+ * opens in a new tab via `allow-top-navigation`.
471
+ */
472
+ mode?: "redirect" | "iframe";
473
+ class?: string;
474
+ style?: SolidStyle;
475
+ title?: string;
476
+ children?: SolidNode;
477
+ loadingFallback?: SolidNode;
478
+ errorFallback?: (error: Error, refresh: () => void) => SolidNode;
479
+ onEvent?: (event: BiabEmbedEvent) => void;
480
+ onSuccess?: (sessionId: string) => void;
481
+ onCancel?: (sessionId: string) => void;
482
+ onError?: (error: Error) => void;
483
+ };
484
+
485
+ /** Start a Stripe checkout, as a button (default) or an embedded surface. */
486
+ export function Checkout(props: CheckoutProps) {
487
+ const client = createMemo(
488
+ () => new BiabDevClient({ apiKey: props.apiKey, baseUrl: props.baseUrl }),
489
+ );
490
+ const [state, setState] = createSignal<
491
+ EmbedSessionState<CheckoutSessionResponse>
492
+ >({ status: "idle" });
493
+
494
+ const start = async (): Promise<void> => {
495
+ setState({ status: "loading" });
496
+ try {
497
+ const session = await client().checkout.createSession(props.cart);
498
+ setState({ status: "ready", session });
499
+ if ((props.mode ?? "redirect") === "redirect") {
500
+ window.location.href = session.stripeUrl;
501
+ }
502
+ } catch (error) {
503
+ const err = error instanceof Error ? error : new Error("Checkout failed");
504
+ setState({ status: "error", error: err });
505
+ props.onError?.(err);
506
+ }
507
+ };
508
+
509
+ // Iframe mode mints its session eagerly; redirect mode waits for the click,
510
+ // because starting a checkout the visitor hasn't asked for creates an
511
+ // abandoned Stripe session every time the component mounts.
512
+ createEffect(() => {
513
+ if (
514
+ (props.mode ?? "redirect") !== "redirect" &&
515
+ state().status === "idle"
516
+ ) {
517
+ void start();
518
+ }
519
+ });
520
+
521
+ return (
522
+ <Show
523
+ fallback={
524
+ <button
525
+ class={props.class}
526
+ disabled={state().status === "loading"}
527
+ onClick={() => void start()}
528
+ style={props.style}
529
+ type="button"
530
+ >
531
+ {renderNode(
532
+ props.children ??
533
+ (state().status === "loading" ? "Loading…" : "Checkout"),
534
+ )}
535
+ </button>
536
+ }
537
+ when={(props.mode ?? "redirect") !== "redirect"}
538
+ >
539
+ <EmbedIframe
540
+ class={props.class}
541
+ errorFallback={props.errorFallback}
542
+ extraSandbox="allow-top-navigation"
543
+ loadingFallback={props.loadingFallback}
544
+ onEvent={(event) => {
545
+ props.onEvent?.(event);
546
+ if (event.type === "biab:checkout:success") {
547
+ props.onSuccess?.(event.sessionId);
548
+ } else if (event.type === "biab:checkout:cancel") {
549
+ props.onCancel?.(event.sessionId);
550
+ } else if (event.type === "biab:checkout:error") {
551
+ props.onError?.(new Error(event.message));
552
+ }
553
+ }}
554
+ refresh={start}
555
+ state={state()}
556
+ style={props.style}
557
+ title={props.title ?? "Checkout"}
558
+ />
559
+ </Show>
560
+ );
561
+ }
562
+
563
+ export type CheckoutResultState = {
564
+ status: "loading" | "paid" | "unpaid" | "error";
565
+ error?: Error;
566
+ amountTotalCents?: number | null;
567
+ currency?: string | null;
568
+ customerEmail?: string | null;
569
+ };
570
+
571
+ export type CheckoutResultProps = {
572
+ apiKey: string;
573
+ baseUrl: string;
574
+ orgSlug: string;
575
+ sessionIdParam?: string;
576
+ render?: (state: CheckoutResultState) => SolidNode;
577
+ };
578
+
579
+ /**
580
+ * Pair with `<Checkout>` in redirect mode: drop this on the success URL and it
581
+ * reads `?session_id=…` and reports the resolved payment status.
582
+ */
583
+ export function CheckoutResult(props: CheckoutResultProps) {
584
+ const [state, setState] = createSignal<CheckoutResultState>({
585
+ status: "loading",
586
+ });
587
+
588
+ onMount(() => {
589
+ let cancelled = false;
590
+ onCleanup(() => {
591
+ cancelled = true;
592
+ });
593
+
594
+ const paramName = props.sessionIdParam ?? "session_id";
595
+ const sessionId = new URL(window.location.href).searchParams.get(paramName);
596
+ if (!sessionId) {
597
+ setState({ status: "error", error: new Error("Missing session id.") });
598
+ return;
599
+ }
600
+
601
+ // The public tRPC endpoint — no API key required.
602
+ const trpcBase = props.baseUrl.replace(/\/api\/package\/v1\/?$/, "");
603
+ const requestUrl = `${trpcBase}/api/trpc/store.checkoutSessionStatus?input=${encodeURIComponent(
604
+ JSON.stringify({ json: { orgSlug: props.orgSlug, sessionId } }),
605
+ )}`;
606
+
607
+ // Defensive: a misconfigured baseUrl must not become a `javascript:` fetch.
608
+ const parsed = (() => {
609
+ try {
610
+ return new URL(requestUrl);
611
+ } catch {
612
+ return null;
613
+ }
614
+ })();
615
+ if (
616
+ !parsed ||
617
+ (parsed.protocol !== "https:" && parsed.protocol !== "http:")
618
+ ) {
619
+ setState({
620
+ status: "error",
621
+ error: new Error(`Invalid baseUrl: ${props.baseUrl}`),
622
+ });
623
+ return;
624
+ }
625
+
626
+ fetch(parsed.toString())
627
+ .then((r) => r.json())
628
+ .then((data) => {
629
+ if (cancelled) return;
630
+ const result = data?.result?.data?.json ?? data?.result?.data;
631
+ if (!result) throw new Error("Empty checkout-status response.");
632
+ setState({
633
+ status: result.paymentStatus === "paid" ? "paid" : "unpaid",
634
+ amountTotalCents: result.amountTotalCents,
635
+ currency: result.currency,
636
+ customerEmail: result.customerEmail,
637
+ });
638
+ })
639
+ .catch((err) => {
640
+ if (cancelled) return;
641
+ setState({
642
+ status: "error",
643
+ error: err instanceof Error ? err : new Error(String(err)),
644
+ });
645
+ });
646
+ });
647
+
648
+ return (
649
+ <Show fallback={props.render?.(state())} when={!props.render}>
650
+ <Show when={state().status === "loading"}>
651
+ <p>Loading order…</p>
652
+ </Show>
653
+ <Show when={state().status === "error"}>
654
+ <p>Couldn't load order: {state().error?.message}</p>
655
+ </Show>
656
+ <Show when={state().status === "paid"}>
657
+ <p>
658
+ Thank you! Receipt sent to {state().customerEmail ?? "your email"}.
659
+ </p>
660
+ </Show>
661
+ <Show when={state().status === "unpaid"}>
662
+ <p>Payment status: pending. We'll email you when it finalizes.</p>
663
+ </Show>
664
+ </Show>
665
+ );
666
+ }
667
+
668
+ // ─── Customer portal (Dashboard) ─────────────────────────────────────────────
669
+
670
+ type DashboardContextValue = {
671
+ state: () => EmbedSessionState<DashboardSessionResponse>;
672
+ refresh: () => Promise<void>;
673
+ };
674
+
675
+ const DashboardContext = createContext<DashboardContextValue | null>(null);
676
+ /** Solid 1 exposes `.Provider`; Solid 2 makes the context its own provider. */
677
+ const DashboardCtxProvider = providerFor<DashboardContextValue | null>(
678
+ DashboardContext,
679
+ );
680
+
681
+ export type DashboardProviderProps = {
682
+ apiKey: string;
683
+ baseUrl: string;
684
+ children?: SolidNode;
685
+ refreshLeadMs?: number;
686
+ };
687
+
688
+ /**
689
+ * Mint and hold one customer-portal session for a subtree, so several
690
+ * `<Dashboard>` instances (or your own UI reading `useDashboardSession`) share
691
+ * a session instead of each minting their own.
692
+ */
693
+ export function DashboardProvider(props: DashboardProviderProps) {
694
+ if (!props.apiKey)
695
+ throw new Error("<DashboardProvider> requires an `apiKey`.");
696
+ if (!props.baseUrl) {
697
+ throw new Error("<DashboardProvider> requires a `baseUrl`.");
698
+ }
699
+
700
+ const { state, refresh } = createEmbedSession<DashboardSessionResponse>(
701
+ (client) => client.dashboard.createSession(),
702
+ () => ({
703
+ apiKey: props.apiKey,
704
+ baseUrl: props.baseUrl,
705
+ refreshLeadMs: props.refreshLeadMs,
706
+ }),
707
+ );
708
+
709
+ return (
710
+ <DashboardCtxProvider value={{ state, refresh }}>
711
+ {renderNode(props.children)}
712
+ </DashboardCtxProvider>
713
+ );
714
+ }
715
+
716
+ /** Read the portal session provided by the nearest `<DashboardProvider>`. */
717
+ export function useDashboardSession(): DashboardContextValue {
718
+ const ctx = useContext(DashboardContext);
719
+ if (!ctx) {
720
+ throw new Error(
721
+ "useDashboardSession must be used inside <DashboardProvider>.",
722
+ );
723
+ }
724
+ return ctx;
725
+ }
726
+
727
+ export type DashboardProps = {
728
+ class?: string;
729
+ style?: SolidStyle;
730
+ title?: string;
731
+ loadingFallback?: SolidNode;
732
+ errorFallback?: (error: Error, refresh: () => void) => SolidNode;
733
+ };
734
+
735
+ /** The signed-in customer portal, embedded. Requires `<DashboardProvider>`. */
736
+ export function Dashboard(props: DashboardProps) {
737
+ const ctx = useDashboardSession();
738
+ return (
739
+ <EmbedIframe
740
+ class={props.class}
741
+ errorFallback={props.errorFallback}
742
+ loadingFallback={
743
+ props.loadingFallback ?? (
744
+ <div class={props.class} style={{ ...messageStyle, ...props.style }}>
745
+ Loading dashboard…
746
+ </div>
747
+ )
748
+ }
749
+ refresh={ctx.refresh}
750
+ state={ctx.state()}
751
+ style={props.style}
752
+ title={props.title ?? "Business In A Box Dashboard"}
753
+ />
754
+ );
755
+ }
756
+
757
+ // ─── Tenant auth ─────────────────────────────────────────────────────────────
758
+
759
+ export type AuthLinkProps = {
760
+ returnTo?: string;
761
+ loginHint?: string;
762
+ basePath?: string;
763
+ class?: string;
764
+ style?: SolidStyle;
765
+ children?: SolidNode;
766
+ };
767
+
768
+ /** Link into the auth handler's sign-in route. */
769
+ export function SignIn(props: AuthLinkProps) {
770
+ return (
771
+ <a
772
+ class={props.class}
773
+ href={buildAuthHref("sign-in", props)}
774
+ style={props.style}
775
+ >
776
+ {renderNode(props.children ?? "Sign in")}
777
+ </a>
778
+ );
779
+ }
780
+
781
+ /** Link into the auth handler's sign-up route. */
782
+ export function SignUp(props: AuthLinkProps) {
783
+ return (
784
+ <a
785
+ class={props.class}
786
+ href={buildAuthHref("sign-up", props)}
787
+ style={props.style}
788
+ >
789
+ {renderNode(props.children ?? "Create account")}
790
+ </a>
791
+ );
792
+ }
793
+
794
+ /** Link into the auth handler's sign-out route. */
795
+ export function SignOut(props: AuthLinkProps) {
796
+ return (
797
+ <a
798
+ class={props.class}
799
+ href={buildAuthHref("sign-out", { basePath: props.basePath })}
800
+ style={props.style}
801
+ >
802
+ {renderNode(props.children ?? "Sign out")}
803
+ </a>
804
+ );
805
+ }
806
+
807
+ export type ProgrammaticAuthOptions = {
808
+ returnTo?: string;
809
+ loginHint?: string;
810
+ basePath?: string;
811
+ };
812
+
813
+ /** Imperative sign-in, for router guards and event handlers. Browser-only. */
814
+ export function signIn(options: ProgrammaticAuthOptions = {}): void {
815
+ if (typeof window === "undefined") return;
816
+ window.location.assign(buildAuthHref("sign-in", options));
817
+ }
818
+
819
+ /** Imperative sign-up. Browser-only. */
820
+ export function signUp(options: ProgrammaticAuthOptions = {}): void {
821
+ if (typeof window === "undefined") return;
822
+ window.location.assign(buildAuthHref("sign-up", options));
823
+ }
824
+
825
+ /** Imperative sign-out. Browser-only. */
826
+ export function signOut(options: { basePath?: string } = {}): void {
827
+ if (typeof window === "undefined") return;
828
+ window.location.assign(buildAuthHref("sign-out", options));
829
+ }
830
+
831
+ // The customer session and followers bindings that used to live here — a
832
+ // hand-rolled fetch and a hand-rolled local-storage hint — now come from
833
+ // `account-core` further down this file, shared with every other binding.
834
+
835
+ // ─── Social links ────────────────────────────────────────────────────────────
836
+
837
+ export type SocialLinksProps = {
838
+ /** The company profile's `socials` record (e.g. `storefrontMeta().socials`). */
839
+ socials: unknown;
840
+ /**
841
+ * Icon colour for branded glyphs — a hex without `#`, or a CSS colour
842
+ * keyword. Omit to use each brand's official colour.
843
+ */
844
+ color?: string;
845
+ /** Icon edge length in px (default 20). */
846
+ size?: number;
847
+ class?: string;
848
+ linkClass?: string;
849
+ iconClass?: string;
850
+ };
851
+
852
+ function SocialGlyph(props: {
853
+ iconUrl: string | null;
854
+ size: number;
855
+ class?: string | undefined;
856
+ label: string;
857
+ }) {
858
+ return (
859
+ <Show
860
+ fallback={
861
+ <svg
862
+ aria-hidden="true"
863
+ class={props.class}
864
+ fill="none"
865
+ height={props.size}
866
+ stroke="currentColor"
867
+ stroke-width={SOCIAL_FALLBACK_GLYPH.strokeWidth}
868
+ viewBox={SOCIAL_FALLBACK_GLYPH.viewBox}
869
+ width={props.size}
870
+ >
871
+ <title>{props.label}</title>
872
+ <circle
873
+ cx={SOCIAL_FALLBACK_GLYPH.circle.cx}
874
+ cy={SOCIAL_FALLBACK_GLYPH.circle.cy}
875
+ r={SOCIAL_FALLBACK_GLYPH.circle.r}
876
+ />
877
+ <path d={SOCIAL_FALLBACK_GLYPH.path} />
878
+ </svg>
879
+ }
880
+ when={props.iconUrl}
881
+ >
882
+ {(url) => (
883
+ <img
884
+ alt={props.label}
885
+ class={props.class}
886
+ height={props.size}
887
+ loading="lazy"
888
+ src={url()}
889
+ width={props.size}
890
+ />
891
+ )}
892
+ </Show>
893
+ );
894
+ }
895
+
896
+ /** Every social profile on the company, as icon links in canonical order. */
897
+ export function SocialLinks(props: SocialLinksProps) {
898
+ // Rows, URLs and icon URLs come from `social-core`; only the markup is Solid's.
899
+ const rows = createMemo(() =>
900
+ resolveSocialLinks(props.socials, { color: props.color }),
901
+ );
902
+
903
+ return (
904
+ <Show when={rows().length > 0}>
905
+ <ul class={props.class} style={SOCIAL_LIST_STYLE}>
906
+ <For each={rows()}>
907
+ {(row) => (
908
+ <li>
909
+ <a
910
+ aria-label={row.label}
911
+ class={props.linkClass}
912
+ href={row.url}
913
+ rel="noopener noreferrer"
914
+ target="_blank"
915
+ title={row.label}
916
+ >
917
+ <SocialGlyph
918
+ class={props.iconClass}
919
+ iconUrl={row.iconUrl}
920
+ label={row.label}
921
+ size={props.size ?? 20}
922
+ />
923
+ </a>
924
+ </li>
925
+ )}
926
+ </For>
927
+ </ul>
928
+ </Show>
929
+ );
930
+ }
931
+
932
+ // ─────────────────────────────────────────────────────────────────────────────
933
+ // Native storefront + chat — the non-iframe path
934
+ //
935
+ // `<Storefront>` and `<Chatbot>` above are hosted pages in a sandbox: fast to
936
+ // adopt, but they render BusinessDash's markup, not yours. These primitives are
937
+ // the alternative. All of the behaviour lives in `store-core` / `chat-core`,
938
+ // shared with every other binding; this only bridges the controller's
939
+ // subscribe/snapshot observable into a Solid signal.
940
+ // ─────────────────────────────────────────────────────────────────────────────
941
+
942
+ /**
943
+ * Bridge a controller with a `subscribe`/`snapshot` observable into a signal.
944
+ *
945
+ * The core does NOT replay on subscribe, so the signal is seeded from
946
+ * `snapshot()` and only then subscribed — without the seed the first render
947
+ * would have nothing to draw.
948
+ */
949
+ function bridge<
950
+ TSnapshot,
951
+ TController extends {
952
+ snapshot(): TSnapshot;
953
+ subscribe(cb: (s: TSnapshot) => void): () => void;
954
+ destroy(): void;
955
+ },
956
+ >(
957
+ create: () => TController,
958
+ start?: (controller: TController) => void,
959
+ ): { controller: () => TController; snapshot: () => TSnapshot } {
960
+ const controller = createMemo(create);
961
+ // Boxed: Solid 2 types the value overload of `createSignal` as
962
+ // `Exclude<T, Function>` because a bare function argument is read as a
963
+ // compute function. A wrapper object is never callable, so this compiles on
964
+ // both versions. See `fromStore` in solid-compat for the same reasoning.
965
+ const [snapshot, setSnapshot] = createSignal<{ value: TSnapshot }>({
966
+ value: controller().snapshot(),
967
+ });
968
+
969
+ createEffect(() => {
970
+ const current = controller();
971
+ setSnapshot({ value: current.snapshot() });
972
+ const unsubscribe = current.subscribe((next) =>
973
+ setSnapshot({ value: next }),
974
+ );
975
+ start?.(current);
976
+ onCleanup(() => {
977
+ unsubscribe();
978
+ current.destroy();
979
+ });
980
+ });
981
+
982
+ // Unwrap the box at the boundary so callers still get a plain accessor.
983
+ return { controller, snapshot: () => snapshot().value };
984
+ }
985
+
986
+ export type CreateStorefrontOptions = {
987
+ apiKey: string;
988
+ baseUrl: string;
989
+ pageSize?: number;
990
+ categoryId?: string | null;
991
+ };
992
+
993
+ /**
994
+ * Headless storefront + cart for Solid.
995
+ *
996
+ * ```tsx
997
+ * const store = createStorefront(() => ({ apiKey, baseUrl }));
998
+ * <For each={store.snapshot().products}>{(p) => <MyCard product={p} />}</For>
999
+ * ```
1000
+ */
1001
+ export function createStorefront(options: () => CreateStorefrontOptions) {
1002
+ const { controller, snapshot } = bridge(
1003
+ () =>
1004
+ createStoreController({
1005
+ client: new BiabDevClient({
1006
+ apiKey: options().apiKey,
1007
+ baseUrl: options().baseUrl,
1008
+ }),
1009
+ ...(options().pageSize ? { pageSize: options().pageSize } : {}),
1010
+ ...(options().categoryId ? { categoryId: options().categoryId } : {}),
1011
+ }),
1012
+ (c) => void c.load(),
1013
+ );
1014
+
1015
+ return {
1016
+ snapshot,
1017
+ loadMore: () => controller().loadMore(),
1018
+ filterByCategory: (id: string | null) => controller().filterByCategory(id),
1019
+ openProduct: (id: string) => controller().openProduct(id),
1020
+ closeProduct: () => controller().closeProduct(),
1021
+ addToCart: (input: CartAddItemInput) => controller().addToCart(input),
1022
+ updateQuantity: (itemId: string, quantity: number) =>
1023
+ controller().updateQuantity(itemId, quantity),
1024
+ removeItem: (itemId: string) => controller().removeItem(itemId),
1025
+ applyCoupon: (code: string) => controller().applyCoupon(code),
1026
+ removeCoupon: () => controller().removeCoupon(),
1027
+ clearCart: () => controller().clearCart(),
1028
+ refreshCart: () => controller().refreshCart(),
1029
+ };
1030
+ }
1031
+
1032
+ export type CreateChatOptions = { apiKey: string; baseUrl: string };
1033
+
1034
+ /**
1035
+ * Headless Front Desk chat for Solid — the native alternative to `<Chatbot>`.
1036
+ *
1037
+ * ```tsx
1038
+ * const chat = createChat(() => ({ apiKey, baseUrl }));
1039
+ * <For each={chat.snapshot().messages}>{(m) => <Bubble message={m} />}</For>
1040
+ * ```
1041
+ */
1042
+ export function createChat(options: () => CreateChatOptions) {
1043
+ const { controller, snapshot } = bridge(() =>
1044
+ createChatController({
1045
+ client: new BiabDevClient({
1046
+ apiKey: options().apiKey,
1047
+ baseUrl: options().baseUrl,
1048
+ }),
1049
+ }),
1050
+ );
1051
+
1052
+ return {
1053
+ snapshot,
1054
+ send: (content: string) => controller().send(content),
1055
+ reset: () => controller().reset(),
1056
+ loadConfig: () => controller().loadConfig(),
1057
+ refreshAvailability: () => controller().refreshAvailability(),
1058
+ getForm: (slug: string) => controller().getForm(slug),
1059
+ submitForm: (slug: string, input: ChatbotFormSubmitInput) =>
1060
+ controller().submitForm(slug, input),
1061
+ requestHuman: (input: ChatbotRequestHumanInput) =>
1062
+ controller().requestHuman(input),
1063
+ };
1064
+ }
1065
+
1066
+ export type CreateBlogOptions = {
1067
+ apiKey: string;
1068
+ baseUrl: string;
1069
+ pageSize?: number;
1070
+ categoryId?: string | null;
1071
+ };
1072
+
1073
+ /**
1074
+ * Headless blog for Solid — the native alternative to `<Blog>`.
1075
+ *
1076
+ * ```tsx
1077
+ * const blog = createBlog(() => ({ apiKey, baseUrl }));
1078
+ * <For each={blog.snapshot().posts}>{(p) => <MyCard post={p} />}</For>
1079
+ * ```
1080
+ */
1081
+ export function createBlog(options: () => CreateBlogOptions) {
1082
+ const { controller, snapshot } = bridge(
1083
+ () =>
1084
+ createBlogController({
1085
+ client: new BiabDevClient({
1086
+ apiKey: options().apiKey,
1087
+ baseUrl: options().baseUrl,
1088
+ }),
1089
+ ...(options().pageSize ? { pageSize: options().pageSize } : {}),
1090
+ ...(options().categoryId ? { categoryId: options().categoryId } : {}),
1091
+ }),
1092
+ (c) => void c.load(),
1093
+ );
1094
+
1095
+ return {
1096
+ snapshot,
1097
+ loadMore: () => controller().loadMore(),
1098
+ filterByCategory: (id: string | null) => controller().filterByCategory(id),
1099
+ openPost: (slug: string) => controller().openPost(slug),
1100
+ closePost: () => controller().closePost(),
1101
+ loadComments: () => controller().loadComments(),
1102
+ loadMoreComments: () => controller().loadMoreComments(),
1103
+ postComment: (input: BlogCommentPostInput) =>
1104
+ controller().postComment(input),
1105
+ toggleLike: () => controller().toggleLike(),
1106
+ };
1107
+ }
1108
+
1109
+ export type CreateCheckoutOptions = { apiKey: string; baseUrl: string };
1110
+
1111
+ /** Headless checkout. `start()` mints a session; `redirect()` navigates. */
1112
+ export function createCheckout(options: () => CreateCheckoutOptions) {
1113
+ const { controller, snapshot } = bridge(() =>
1114
+ createCheckoutController({
1115
+ client: new BiabDevClient({
1116
+ apiKey: options().apiKey,
1117
+ baseUrl: options().baseUrl,
1118
+ }),
1119
+ }),
1120
+ );
1121
+ return {
1122
+ snapshot,
1123
+ start: (input: CheckoutSessionInput) => controller().start(input),
1124
+ redirect: () => controller().redirect(),
1125
+ resolve: (sessionId: string) => controller().resolve(sessionId),
1126
+ reset: () => controller().reset(),
1127
+ };
1128
+ }
1129
+
1130
+ /**
1131
+ * The signed-in customer, from the auth handler's `/me`.
1132
+ *
1133
+ * Replaces the hand-rolled fetch this binding used to carry: the logic now
1134
+ * lives in `account-core` alongside every other framework's.
1135
+ */
1136
+ export function createUser(options: () => { basePath?: string } = () => ({})) {
1137
+ const { controller, snapshot } = bridge(
1138
+ () => createUserController(options()),
1139
+ (c) => c.start(),
1140
+ );
1141
+ return { snapshot, refresh: () => controller().refresh() };
1142
+ }
1143
+
1144
+ export type CreateFollowersOptions = {
1145
+ token: string;
1146
+ siteId: string;
1147
+ baseUrl?: string;
1148
+ };
1149
+
1150
+ /** Newsletter / follower subscribe with the local already-subscribed hint. */
1151
+ export function createFollowers(options: () => CreateFollowersOptions) {
1152
+ const { controller, snapshot } = bridge(() =>
1153
+ createFollowersController({
1154
+ client: createBiabClient({
1155
+ apiKey: options().token,
1156
+ siteId: options().siteId,
1157
+ ...(options().baseUrl ? { baseUrl: options().baseUrl } : {}),
1158
+ }),
1159
+ siteId: options().siteId,
1160
+ }),
1161
+ );
1162
+ return {
1163
+ snapshot,
1164
+ join: (input: { email: string; name?: string; source?: string }) =>
1165
+ controller().join(input),
1166
+ leave: (email: string) => controller().leave(email),
1167
+ me: (email: string) => controller().me(email),
1168
+ edit: (email: string, patch: { name?: string; source?: string }) =>
1169
+ controller().edit(email, patch),
1170
+ };
1171
+ }
1172
+
1173
+ export type CreateMarketingOptions = {
1174
+ token: string;
1175
+ siteId: string;
1176
+ baseUrl?: string;
1177
+ };
1178
+
1179
+ /** Headless marketing pages — the index, plus one page at a time. */
1180
+ export function createMarketing(options: () => CreateMarketingOptions) {
1181
+ const { controller, snapshot } = bridge(
1182
+ () =>
1183
+ createMarketingController({
1184
+ client: createBiabClient({
1185
+ apiKey: options().token,
1186
+ siteId: options().siteId,
1187
+ ...(options().baseUrl ? { baseUrl: options().baseUrl } : {}),
1188
+ }),
1189
+ }),
1190
+ (c) => void c.load(),
1191
+ );
1192
+ return {
1193
+ snapshot,
1194
+ open: (pageKey?: string, opts?: { skipCache?: boolean }) =>
1195
+ controller().open(pageKey, opts),
1196
+ prefetch: (pageKey?: string) => controller().prefetch(pageKey),
1197
+ close: () => controller().close(),
1198
+ };
1199
+ }
1200
+
1201
+ export type CreateVerifyOptions = {
1202
+ apiKey: string;
1203
+ baseUrl: string;
1204
+ } & Omit<CreateVerifyControllerOptions, "client">;
1205
+
1206
+ /** Headless email / phone verification. Read `snapshot().flow` before rendering. */
1207
+ export function createVerify(options: () => CreateVerifyOptions) {
1208
+ const { controller, snapshot } = bridge(() => {
1209
+ const { apiKey, baseUrl, ...rest } = options();
1210
+ return createVerifyController({
1211
+ client: new BiabDevClient({ apiKey, baseUrl }),
1212
+ ...rest,
1213
+ });
1214
+ });
1215
+ return {
1216
+ snapshot,
1217
+ start: (destination: string) => controller().start(destination),
1218
+ resend: () => controller().resend(),
1219
+ confirm: (token: string) => controller().confirm(token),
1220
+ canResend: (now?: number) => controller().canResend(now),
1221
+ reset: () => controller().reset(),
1222
+ };
1223
+ }
1224
+
1225
+ export type CreatePortalOptions = {
1226
+ apiKey: string;
1227
+ baseUrl: string;
1228
+ sessionToken: string;
1229
+ };
1230
+
1231
+ /**
1232
+ * The signed-in customer's portal — work, quotes, contracts, invoices, orders,
1233
+ * downloads, shipping, reviews, referrals, profile and notification settings.
1234
+ *
1235
+ * `sessionToken` is required: every portal route is scoped to the signed-in
1236
+ * customer, and without one the portal renders empty rather than signed-out.
1237
+ *
1238
+ * There are no list endpoints for invoices, quotes, contracts or shipments —
1239
+ * `snapshot().work` carries the counts and you open each by id. A submitted
1240
+ * review is queued for staff approval, so it will not appear in
1241
+ * `snapshot().reviews`; watch `reviewAwaitingModeration`.
1242
+ */
1243
+ export function createPortal(options: () => CreatePortalOptions) {
1244
+ const { controller, snapshot } = bridge(
1245
+ () =>
1246
+ createPortalController({
1247
+ client: new BiabDevClient({
1248
+ apiKey: options().apiKey,
1249
+ baseUrl: options().baseUrl,
1250
+ }),
1251
+ sessionToken: options().sessionToken,
1252
+ }),
1253
+ (c) => void c.load(),
1254
+ );
1255
+
1256
+ return {
1257
+ snapshot,
1258
+ load: () => controller().load(),
1259
+ loadOrders: (opts?: { limit?: number }) => controller().loadOrders(opts),
1260
+ loadInvoices: (opts?: {
1261
+ limit?: number;
1262
+ status?: string;
1263
+ unpaid?: boolean;
1264
+ }) => controller().loadInvoices(opts),
1265
+ loadQuotes: (opts?: { limit?: number; status?: string }) =>
1266
+ controller().loadQuotes(opts),
1267
+ loadContracts: (opts?: { limit?: number; status?: string }) =>
1268
+ controller().loadContracts(opts),
1269
+ loadShipments: (opts?: { limit?: number; active?: boolean }) =>
1270
+ controller().loadShipments(opts),
1271
+ loadMessages: (opts?: { limit?: number; jobId?: string }) =>
1272
+ controller().loadMessages(opts),
1273
+ postMessage: (input: CustomerPortalMessagePostInput) =>
1274
+ controller().postMessage(input),
1275
+ markMessagesRead: () => controller().markMessagesRead(),
1276
+ loadPayments: (opts?: { limit?: number }) =>
1277
+ controller().loadPayments(opts),
1278
+ loadReviews: () => controller().loadReviews(),
1279
+ loadReferrals: () => controller().loadReferrals(),
1280
+ loadNotificationPreferences: () =>
1281
+ controller().loadNotificationPreferences(),
1282
+ loadOtherOrgs: () => controller().loadOtherOrgs(),
1283
+ openJob: (id: string) => controller().openJob(id),
1284
+ openOrder: (id: string) => controller().openOrder(id),
1285
+ openInvoice: (id: string) => controller().openInvoice(id),
1286
+ openQuote: (id: string) => controller().openQuote(id),
1287
+ openContract: (id: string) => controller().openContract(id),
1288
+ trackShipment: (id: string) => controller().trackShipment(id),
1289
+ closeDetail: () => controller().closeDetail(),
1290
+ postJobComment: (jobId: string, input: CustomerJobCommentPostInput) =>
1291
+ controller().postJobComment(jobId, input),
1292
+ acceptQuote: (id: string) => controller().acceptQuote(id),
1293
+ rejectQuote: (id: string) => controller().rejectQuote(id),
1294
+ signContract: (id: string) => controller().signContract(id),
1295
+ payInvoice: (id: string, opts?: CustomerInvoicePayInput) =>
1296
+ controller().payInvoice(id, opts),
1297
+ updateProfile: (input: CustomerPortalUpdateProfileInput) =>
1298
+ controller().updateProfile(input),
1299
+ updateNotificationPreferences: (
1300
+ input: NotificationPreferencesUpdateInput,
1301
+ ) => controller().updateNotificationPreferences(input),
1302
+ submitReview: (input: CustomerReviewSubmitInput) =>
1303
+ controller().submitReview(input),
1304
+ submitReturnRequest: (
1305
+ orderId: string,
1306
+ input?: CustomerReturnRequestInput,
1307
+ ) => controller().submitReturnRequest(orderId, input),
1308
+ contactCompany: (input: CustomerContactCompanyInput) =>
1309
+ controller().contactCompany(input),
1310
+ };
1311
+ }