@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.
- package/README.md +991 -295
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +622 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +888 -1
- package/dist/client.js.map +1 -1
- package/dist/consent-core/index.cjs +107 -0
- package/dist/consent-core/index.d.ts +103 -0
- package/dist/consent-core/index.d.ts.map +1 -0
- package/dist/consent-core/index.js +157 -0
- package/dist/consent-core/index.js.map +1 -0
- package/dist/contracts.d.ts +3526 -767
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +571 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1355 -24
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4516 -1311
- package/package.json +128 -11
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +1097 -0
- package/src/consent-core/index.ts +213 -0
- package/src/contracts.ts +768 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- 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
|
+
}
|