@base316/react 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1208 @@
1
+ "use client";
2
+
3
+ // src/provider.tsx
4
+ import { Base316Browser } from "@base316/sdk";
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ useState
12
+ } from "react";
13
+
14
+ // src/appearance.ts
15
+ var VARIABLES = {
16
+ colorPrimary: "--b316-color-primary",
17
+ colorPrimaryText: "--b316-color-primary-text",
18
+ colorText: "--b316-color-text",
19
+ colorMuted: "--b316-color-muted",
20
+ colorBackground: "--b316-color-background",
21
+ colorBorder: "--b316-color-border",
22
+ colorDanger: "--b316-color-danger",
23
+ colorDangerBackground: "--b316-color-danger-bg",
24
+ colorSuccess: "--b316-color-success",
25
+ borderRadius: "--b316-radius",
26
+ fontFamily: "--b316-font",
27
+ fontSize: "--b316-font-size",
28
+ spacing: "--b316-spacing"
29
+ };
30
+ function appearanceToStyle(appearance) {
31
+ if (!appearance) {
32
+ return {};
33
+ }
34
+ const style = {};
35
+ for (const [key, variable] of Object.entries(VARIABLES)) {
36
+ const value = appearance[key];
37
+ if (value !== void 0) {
38
+ style[variable] = value;
39
+ }
40
+ }
41
+ return style;
42
+ }
43
+ var STYLE_ELEMENT_ID = "base316-components";
44
+ var STYLESHEET = `
45
+ .b316-root {
46
+ --b316-color-primary: #111827;
47
+ --b316-color-primary-text: #ffffff;
48
+ --b316-color-text: #111827;
49
+ --b316-color-muted: #6b7280;
50
+ --b316-color-background: #ffffff;
51
+ --b316-color-border: #e5e7eb;
52
+ --b316-color-danger: #991b1b;
53
+ --b316-color-danger-bg: #fef2f2;
54
+ --b316-color-success: #166534;
55
+ --b316-radius: 12px;
56
+ --b316-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
57
+ --b316-font-size: 15px;
58
+ --b316-spacing: 12px;
59
+
60
+ font-family: var(--b316-font);
61
+ font-size: var(--b316-font-size);
62
+ line-height: 1.5;
63
+ color: var(--b316-color-text);
64
+ }
65
+
66
+ .b316-card {
67
+ border: 1px solid var(--b316-color-border);
68
+ border-radius: var(--b316-radius);
69
+ padding: calc(var(--b316-spacing) * 1.6);
70
+ background: var(--b316-color-background);
71
+ }
72
+
73
+ .b316-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: var(--b316-spacing); }
74
+ .b316-label { font-size: 0.87em; color: var(--b316-color-muted); }
75
+
76
+ .b316-input {
77
+ padding: 9px 11px;
78
+ border: 1px solid var(--b316-color-border);
79
+ border-radius: calc(var(--b316-radius) / 1.6);
80
+ font: inherit;
81
+ color: inherit;
82
+ background: var(--b316-color-background);
83
+ }
84
+ .b316-input:focus-visible { outline: 2px solid var(--b316-color-primary); outline-offset: 1px; }
85
+
86
+ .b316-button {
87
+ width: 100%;
88
+ padding: 10px 14px;
89
+ border: 0;
90
+ border-radius: calc(var(--b316-radius) / 1.6);
91
+ background: var(--b316-color-primary);
92
+ color: var(--b316-color-primary-text);
93
+ font: inherit;
94
+ font-weight: 600;
95
+ cursor: pointer;
96
+ }
97
+ .b316-button:hover:not([disabled]) { filter: brightness(1.12); }
98
+ .b316-button:active:not([disabled]) { transform: translateY(1px); }
99
+ .b316-button:focus-visible { outline: 2px solid var(--b316-color-primary); outline-offset: 2px; }
100
+ .b316-button[disabled] { opacity: .55; cursor: not-allowed; }
101
+
102
+ .b316-button-secondary {
103
+ width: auto;
104
+ transition: background-color .12s ease;
105
+ background: var(--b316-color-background);
106
+ color: var(--b316-color-text);
107
+ border: 1px solid var(--b316-color-border);
108
+ font-weight: 500;
109
+ }
110
+
111
+ .b316-button-link {
112
+ width: auto;
113
+ background: none;
114
+ border: 0;
115
+ padding: 0;
116
+ color: var(--b316-color-primary);
117
+ font: inherit;
118
+ cursor: pointer;
119
+ text-decoration: underline;
120
+ }
121
+
122
+ .b316-error {
123
+ background: var(--b316-color-danger-bg);
124
+ color: var(--b316-color-danger);
125
+ padding: 8px 10px;
126
+ border-radius: calc(var(--b316-radius) / 2);
127
+ font-size: 0.9em;
128
+ margin-bottom: var(--b316-spacing);
129
+ }
130
+
131
+ .b316-muted { color: var(--b316-color-muted); font-size: 0.87em; }
132
+ .b316-success { color: var(--b316-color-success); }
133
+
134
+ .b316-plans {
135
+ display: grid;
136
+ gap: calc(var(--b316-spacing) * 1.3);
137
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
138
+ align-items: stretch;
139
+ }
140
+
141
+ .b316-plan {
142
+ display: flex;
143
+ flex-direction: column;
144
+ padding: 0;
145
+ overflow: hidden;
146
+ }
147
+
148
+ /* The recommended plan. Lifted rather than merely tinted: on a pricing table
149
+ the eye needs one obvious default, and colour alone does not survive being
150
+ viewed in greyscale or by someone who cannot distinguish the accent. */
151
+ .b316-plan-featured {
152
+ border-color: var(--b316-color-primary);
153
+ box-shadow: 0 0 0 1px var(--b316-color-primary);
154
+ }
155
+
156
+ .b316-plan-badge {
157
+ display: inline-block;
158
+ background: var(--b316-color-primary);
159
+ color: var(--b316-color-primary-text);
160
+ border-radius: 999px;
161
+ padding: 2px 9px;
162
+ font-size: 0.75em;
163
+ font-weight: 600;
164
+ letter-spacing: .02em;
165
+ margin-bottom: 8px;
166
+ }
167
+
168
+ /* Occupies the badge's height without drawing anything, so a featured card
169
+ does not push its own price and features out of line with the others. */
170
+ .b316-plan-badge-spacer {
171
+ display: inline-block;
172
+ padding: 2px 9px;
173
+ font-size: 0.75em;
174
+ margin-bottom: 8px;
175
+ visibility: hidden;
176
+ }
177
+
178
+ .b316-plan-head { padding: calc(var(--b316-spacing) * 1.6); padding-bottom: 0; }
179
+ .b316-plan-name { font-weight: 600; font-size: 1.06em; }
180
+ .b316-plan-desc { color: var(--b316-color-muted); font-size: 0.87em; margin-top: 2px; }
181
+
182
+ .b316-plan-pricing { padding: 0 calc(var(--b316-spacing) * 1.6) calc(var(--b316-spacing) * 1.4); }
183
+
184
+ .b316-plan-price {
185
+ font-size: 2.1em;
186
+ font-weight: 640;
187
+ letter-spacing: -.02em;
188
+ margin-top: calc(var(--b316-spacing) * 0.7);
189
+ /* Lining figures of even width, so prices do not shimmer between cards. */
190
+ font-variant-numeric: tabular-nums lining-nums;
191
+ }
192
+
193
+ .b316-plan-cadence { color: var(--b316-color-muted); font-size: 0.87em; min-height: 1.35em; }
194
+ .b316-plan-trial { color: var(--b316-color-success); font-size: 0.87em; min-height: 1.35em; }
195
+
196
+ .b316-plan-features {
197
+ list-style: none;
198
+ padding: calc(var(--b316-spacing) * 1.2) calc(var(--b316-spacing) * 1.6);
199
+ margin: 0;
200
+ flex: 1;
201
+ border-top: 1px solid var(--b316-color-border);
202
+ }
203
+ .b316-plan-feature {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ gap: 12px;
207
+ padding: 5px 0;
208
+ font-size: 0.94em;
209
+ }
210
+ .b316-plan-feature-value { color: var(--b316-color-muted); font-variant-numeric: tabular-nums; }
211
+
212
+ .b316-plan-action {
213
+ padding: calc(var(--b316-spacing) * 1.2) calc(var(--b316-spacing) * 1.6);
214
+ border-top: 1px solid var(--b316-color-border);
215
+ }
216
+
217
+ /* The plan already held. A faded primary button reads as broken rather than
218
+ as "you are here", so this is a flat, quiet, genuinely inert surface. */
219
+ .b316-button-current,
220
+ .b316-button-current[disabled] {
221
+ background: transparent;
222
+ color: var(--b316-color-muted);
223
+ border: 1px solid var(--b316-color-border);
224
+ font-weight: 500;
225
+ opacity: 1;
226
+ cursor: default;
227
+ }
228
+
229
+ .b316-row { display: flex; justify-content: space-between; gap: 16px; align-items: center; }
230
+ `;
231
+ function cls(base, extra) {
232
+ return extra ? `${base} ${extra}` : base;
233
+ }
234
+ var STYLESHEET_EXTRA = `
235
+ .b316-branding { display: flex; flex-direction: column; gap: 4px; }
236
+ .b316-branding-logo { max-height: 40px; width: auto; align-self: flex-start; }
237
+ .b316-branding-name { font-size: 1.5em; font-weight: 650; letter-spacing: -.01em; }
238
+ .b316-branding-tagline { color: var(--b316-color-muted); }
239
+
240
+ .b316-invoice-table { width: 100%; border-collapse: collapse; font-size: 0.94em; }
241
+ .b316-invoice-table th {
242
+ text-align: left;
243
+ font-weight: 500;
244
+ color: var(--b316-color-muted);
245
+ font-size: 0.85em;
246
+ padding: 0 0 6px;
247
+ border-bottom: 1px solid var(--b316-color-border);
248
+ }
249
+ .b316-invoice-table td { padding: 9px 0; border-bottom: 1px solid var(--b316-color-border); }
250
+ /* Columns need breathing room or the amount runs straight into the action \u2014
251
+ "$49.00View" \u2014 which is what happens when only the row has padding. */
252
+ .b316-invoice-table th + th,
253
+ .b316-invoice-table td + td { padding-left: 16px; }
254
+ .b316-invoice-table td:last-child,
255
+ .b316-invoice-table th:last-child { text-align: right; width: 1%; white-space: nowrap; }
256
+ .b316-invoice-table tr:last-child td { border-bottom: 0; }
257
+ .b316-num { text-align: right; font-variant-numeric: tabular-nums; }
258
+
259
+ .b316-invoice-view {
260
+ margin-top: calc(var(--b316-spacing) * 1.4);
261
+ border-top: 1px solid var(--b316-color-border);
262
+ padding-top: calc(var(--b316-spacing) * 1.4);
263
+ }
264
+ .b316-invoice-actions { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: var(--b316-spacing); }
265
+ .b316-invoice-doc { display: flex; flex-direction: column; gap: calc(var(--b316-spacing) * 1.4); }
266
+ .b316-invoice-meta { display: grid; grid-template-columns: 1fr 1fr; gap: var(--b316-spacing); }
267
+ .b316-invoice-totals { display: flex; flex-direction: column; gap: 6px; margin-left: auto; min-width: 240px; }
268
+ .b316-invoice-total { font-weight: 650; border-top: 1px solid var(--b316-color-border); padding-top: 6px; }
269
+
270
+ /* Printing emits the invoice alone. Without this the browser would paginate the
271
+ whole application around it, which is not something anyone wants to file. */
272
+ @media print {
273
+ body * { visibility: hidden; }
274
+ .b316-invoice-doc, .b316-invoice-doc * { visibility: visible; }
275
+ .b316-invoice-doc {
276
+ position: absolute;
277
+ inset: 0 auto auto 0;
278
+ width: 100%;
279
+ padding: 24px;
280
+ }
281
+ .b316-invoice-actions { display: none; }
282
+ }
283
+ `;
284
+
285
+ // src/provider.tsx
286
+ import { jsx } from "react/jsx-runtime";
287
+ var Base316Context = createContext(null);
288
+ var STORAGE_KEY = "base316.session";
289
+ function readStoredToken() {
290
+ try {
291
+ return window.localStorage.getItem(STORAGE_KEY);
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+ function writeStoredToken(token) {
297
+ try {
298
+ if (token === null) {
299
+ window.localStorage.removeItem(STORAGE_KEY);
300
+ } else {
301
+ window.localStorage.setItem(STORAGE_KEY, token);
302
+ }
303
+ } catch {
304
+ }
305
+ }
306
+ function Base316Provider({
307
+ publishableKey,
308
+ baseUrl,
309
+ token: controlledToken,
310
+ onTokenChange,
311
+ appearance,
312
+ className,
313
+ children
314
+ }) {
315
+ const client = useMemo(
316
+ () => new Base316Browser({ publishableKey, baseUrl }),
317
+ [publishableKey, baseUrl]
318
+ );
319
+ useEffect(() => {
320
+ if (document.getElementById(STYLE_ELEMENT_ID)) {
321
+ return;
322
+ }
323
+ const element = document.createElement("style");
324
+ element.id = STYLE_ELEMENT_ID;
325
+ element.textContent = STYLESHEET + STYLESHEET_EXTRA;
326
+ document.head.appendChild(element);
327
+ }, []);
328
+ const controlled = controlledToken !== void 0;
329
+ const [internalToken, setInternalToken] = useState(null);
330
+ const [session, setSession] = useState(null);
331
+ const [branding, setBranding] = useState(null);
332
+ const [loading, setLoading] = useState(true);
333
+ const token = controlled ? controlledToken : internalToken;
334
+ useEffect(() => {
335
+ if (controlled) {
336
+ return;
337
+ }
338
+ setInternalToken(readStoredToken());
339
+ }, [controlled]);
340
+ useEffect(() => {
341
+ let cancelled = false;
342
+ client.billing.branding().then((result) => {
343
+ if (!cancelled) setBranding(result);
344
+ }).catch(() => {
345
+ });
346
+ return () => {
347
+ cancelled = true;
348
+ };
349
+ }, [client]);
350
+ const setToken = useCallback(
351
+ (next) => {
352
+ if (controlled) {
353
+ onTokenChange?.(next);
354
+ return;
355
+ }
356
+ writeStoredToken(next);
357
+ setInternalToken(next);
358
+ onTokenChange?.(next);
359
+ },
360
+ [controlled, onTokenChange]
361
+ );
362
+ const refresh = useCallback(async () => {
363
+ if (!token) {
364
+ setSession(null);
365
+ setLoading(false);
366
+ return;
367
+ }
368
+ try {
369
+ setSession(await client.auth.session(token));
370
+ } catch {
371
+ setSession(null);
372
+ setToken(null);
373
+ } finally {
374
+ setLoading(false);
375
+ }
376
+ }, [client, token, setToken]);
377
+ useEffect(() => {
378
+ setLoading(true);
379
+ void refresh();
380
+ }, [refresh]);
381
+ const signOut = useCallback(async () => {
382
+ if (token) {
383
+ await client.auth.signOut(token).catch(() => {
384
+ });
385
+ }
386
+ setToken(null);
387
+ setSession(null);
388
+ }, [client, token, setToken]);
389
+ const value = useMemo(
390
+ () => ({ client, branding, token, setToken, session, loading, refresh, signOut }),
391
+ [client, branding, token, setToken, session, loading, refresh, signOut]
392
+ );
393
+ return /* @__PURE__ */ jsx(Base316Context.Provider, { value, children: /* @__PURE__ */ jsx(
394
+ "div",
395
+ {
396
+ className: className ? `b316-root ${className}` : "b316-root",
397
+ style: appearanceToStyle({
398
+ // Whatever was set in the sign-in designer arrives here, so a
399
+ // customer styles their login once on Base316 and the embedded
400
+ // components follow — rather than configuring the same colours
401
+ // twice and watching them drift apart.
402
+ //
403
+ // An explicit `appearance` still wins, because a host that has
404
+ // gone to the trouble of passing tokens means them.
405
+ ...branding ? {
406
+ colorPrimary: branding.primaryColor,
407
+ colorBackground: branding.backgroundColor
408
+ } : {},
409
+ ...appearance
410
+ }),
411
+ children
412
+ }
413
+ ) });
414
+ }
415
+ function useBase316() {
416
+ const value = useContext(Base316Context);
417
+ if (!value) {
418
+ throw new Error("Base316 components must be rendered inside <Base316Provider>.");
419
+ }
420
+ return value;
421
+ }
422
+ function useSession() {
423
+ const { session, loading } = useBase316();
424
+ return { session, loading };
425
+ }
426
+
427
+ // src/Branding.tsx
428
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
429
+ function Branding({
430
+ showTagline = true,
431
+ className,
432
+ style
433
+ }) {
434
+ const { branding } = useBase316();
435
+ if (!branding) {
436
+ return null;
437
+ }
438
+ const showLogo = branding.logoUrl && branding.brandDisplay !== "NAME";
439
+ const showName = branding.brandDisplay !== "LOGO" || !branding.logoUrl;
440
+ return /* @__PURE__ */ jsxs("div", { className: cls("b316-branding", className), style, children: [
441
+ showLogo ? /* @__PURE__ */ jsx2("img", { className: "b316-branding-logo", src: branding.logoUrl, alt: branding.brandName }) : null,
442
+ showName ? /* @__PURE__ */ jsx2("div", { className: "b316-branding-name", children: branding.brandName }) : null,
443
+ showTagline && branding.tagline ? /* @__PURE__ */ jsx2("div", { className: "b316-branding-tagline", children: branding.tagline }) : null
444
+ ] });
445
+ }
446
+
447
+ // src/Invoices.tsx
448
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
449
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
450
+ function money(amount, currency) {
451
+ const value = Number(amount);
452
+ if (!Number.isFinite(value)) {
453
+ return `${amount} ${currency}`;
454
+ }
455
+ try {
456
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(value);
457
+ } catch {
458
+ return `${value} ${currency}`;
459
+ }
460
+ }
461
+ function day(value) {
462
+ return value ? new Date(value).toLocaleDateString() : "\u2014";
463
+ }
464
+ function Invoices({ className, style }) {
465
+ const { client, token, loading: sessionLoading } = useBase316();
466
+ const [invoices, setInvoices] = useState2(null);
467
+ const [open, setOpen] = useState2(null);
468
+ const [error, setError] = useState2(null);
469
+ useEffect2(() => {
470
+ if (!token) {
471
+ setInvoices([]);
472
+ return;
473
+ }
474
+ let cancelled = false;
475
+ client.billing.invoices(token).then((result) => {
476
+ if (!cancelled) setInvoices(result.data);
477
+ }).catch((cause) => {
478
+ console.error("[base316] could not load invoices", cause);
479
+ if (!cancelled) setError("Could not load your invoices.");
480
+ });
481
+ return () => {
482
+ cancelled = true;
483
+ };
484
+ }, [client, token]);
485
+ const view = useCallback2(
486
+ async (id) => {
487
+ if (!token) return;
488
+ try {
489
+ setOpen(await client.billing.invoice(token, id));
490
+ } catch (cause) {
491
+ console.error("[base316] could not load the invoice", cause);
492
+ setError("Could not open that invoice.");
493
+ }
494
+ },
495
+ [client, token]
496
+ );
497
+ if (sessionLoading || invoices === null) {
498
+ return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
499
+ }
500
+ if (!token) {
501
+ return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: "Sign in to see your invoices." });
502
+ }
503
+ if (invoices.length === 0) {
504
+ return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: "No invoices yet." });
505
+ }
506
+ return /* @__PURE__ */ jsxs2("div", { className: cls("b316-card b316-invoices", className), style, children: [
507
+ error ? /* @__PURE__ */ jsx3("div", { className: "b316-error", children: error }) : null,
508
+ /* @__PURE__ */ jsxs2("table", { className: "b316-invoice-table", children: [
509
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
510
+ /* @__PURE__ */ jsx3("th", { children: "Invoice" }),
511
+ /* @__PURE__ */ jsx3("th", { children: "Period" }),
512
+ /* @__PURE__ */ jsx3("th", { children: "Status" }),
513
+ /* @__PURE__ */ jsx3("th", { className: "b316-num", children: "Total" }),
514
+ /* @__PURE__ */ jsx3("th", {})
515
+ ] }) }),
516
+ /* @__PURE__ */ jsx3("tbody", { children: invoices.map((invoice) => /* @__PURE__ */ jsxs2("tr", { children: [
517
+ /* @__PURE__ */ jsx3("td", { children: invoice.number }),
518
+ /* @__PURE__ */ jsxs2("td", { className: "b316-muted", children: [
519
+ day(invoice.periodStart),
520
+ " \u2013 ",
521
+ day(invoice.periodEnd)
522
+ ] }),
523
+ /* @__PURE__ */ jsx3("td", { className: "b316-muted", children: invoice.status.toLowerCase() }),
524
+ /* @__PURE__ */ jsx3("td", { className: "b316-num", children: money(invoice.total, invoice.currency) }),
525
+ /* @__PURE__ */ jsx3("td", { children: /* @__PURE__ */ jsx3(
526
+ "button",
527
+ {
528
+ type: "button",
529
+ className: "b316-button b316-button-link",
530
+ onClick: () => void view(invoice.id),
531
+ children: "View"
532
+ }
533
+ ) })
534
+ ] }, invoice.id)) })
535
+ ] }),
536
+ open ? /* @__PURE__ */ jsx3(InvoiceView, { invoice: open, onClose: () => setOpen(null) }) : null
537
+ ] });
538
+ }
539
+ function InvoiceView({ invoice, onClose }) {
540
+ return /* @__PURE__ */ jsxs2("div", { className: "b316-invoice-view", children: [
541
+ /* @__PURE__ */ jsxs2("div", { className: "b316-invoice-actions", children: [
542
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "b316-button b316-button-secondary", onClick: onClose, children: "Close" }),
543
+ /* @__PURE__ */ jsx3(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ className: "b316-button b316-button-secondary",
548
+ onClick: () => window.print(),
549
+ children: "Download PDF"
550
+ }
551
+ )
552
+ ] }),
553
+ /* @__PURE__ */ jsxs2("div", { className: "b316-invoice-doc", children: [
554
+ /* @__PURE__ */ jsxs2("div", { className: "b316-row", children: [
555
+ /* @__PURE__ */ jsxs2("div", { children: [
556
+ /* @__PURE__ */ jsx3("div", { className: "b316-plan-name", children: invoice.issuer.brandName }),
557
+ invoice.issuer.supportEmail ? /* @__PURE__ */ jsx3("div", { className: "b316-muted", children: invoice.issuer.supportEmail }) : null
558
+ ] }),
559
+ /* @__PURE__ */ jsxs2("div", { style: { textAlign: "right" }, children: [
560
+ /* @__PURE__ */ jsx3("div", { className: "b316-plan-name", children: invoice.number }),
561
+ /* @__PURE__ */ jsx3("div", { className: "b316-muted", children: invoice.status.toLowerCase() })
562
+ ] })
563
+ ] }),
564
+ /* @__PURE__ */ jsxs2("div", { className: "b316-invoice-meta", children: [
565
+ /* @__PURE__ */ jsxs2("div", { children: [
566
+ /* @__PURE__ */ jsx3("div", { className: "b316-label", children: "Billed to" }),
567
+ /* @__PURE__ */ jsx3("div", { children: invoice.customer.name ?? invoice.customer.email ?? "\u2014" }),
568
+ invoice.customer.name && invoice.customer.email ? /* @__PURE__ */ jsx3("div", { className: "b316-muted", children: invoice.customer.email }) : null
569
+ ] }),
570
+ /* @__PURE__ */ jsxs2("div", { children: [
571
+ /* @__PURE__ */ jsx3("div", { className: "b316-label", children: "Period" }),
572
+ /* @__PURE__ */ jsxs2("div", { children: [
573
+ day(invoice.periodStart),
574
+ " \u2013 ",
575
+ day(invoice.periodEnd)
576
+ ] }),
577
+ /* @__PURE__ */ jsxs2("div", { className: "b316-muted", children: [
578
+ "Issued ",
579
+ day(invoice.issuedAt)
580
+ ] })
581
+ ] })
582
+ ] }),
583
+ /* @__PURE__ */ jsxs2("table", { className: "b316-invoice-table", children: [
584
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
585
+ /* @__PURE__ */ jsx3("th", { children: "Description" }),
586
+ /* @__PURE__ */ jsx3("th", { className: "b316-num", children: "Qty" }),
587
+ /* @__PURE__ */ jsx3("th", { className: "b316-num", children: "Unit" }),
588
+ /* @__PURE__ */ jsx3("th", { className: "b316-num", children: "Amount" })
589
+ ] }) }),
590
+ /* @__PURE__ */ jsx3("tbody", { children: invoice.lineItems.map((line) => /* @__PURE__ */ jsxs2("tr", { children: [
591
+ /* @__PURE__ */ jsx3("td", { children: line.description }),
592
+ /* @__PURE__ */ jsxs2("td", { className: "b316-num b316-muted", children: [
593
+ line.billable,
594
+ line.unit ? ` ${line.unit}` : ""
595
+ ] }),
596
+ /* @__PURE__ */ jsx3("td", { className: "b316-num b316-muted", children: line.unitAmount === null ? "\u2014" : money(line.unitAmount, invoice.currency) }),
597
+ /* @__PURE__ */ jsx3("td", { className: "b316-num", children: money(line.amount, invoice.currency) })
598
+ ] }, line.id)) })
599
+ ] }),
600
+ /* @__PURE__ */ jsxs2("div", { className: "b316-invoice-totals", children: [
601
+ /* @__PURE__ */ jsxs2("div", { className: "b316-row", children: [
602
+ /* @__PURE__ */ jsx3("span", { className: "b316-muted", children: "Subtotal" }),
603
+ /* @__PURE__ */ jsx3("span", { className: "b316-num", children: money(invoice.subtotal, invoice.currency) })
604
+ ] }),
605
+ Number(invoice.discountTotal) > 0 ? /* @__PURE__ */ jsxs2("div", { className: "b316-row", children: [
606
+ /* @__PURE__ */ jsx3("span", { className: "b316-muted", children: "Discount" }),
607
+ /* @__PURE__ */ jsxs2("span", { className: "b316-num", children: [
608
+ "\u2212",
609
+ money(invoice.discountTotal, invoice.currency)
610
+ ] })
611
+ ] }) : null,
612
+ Number(invoice.taxTotal) > 0 ? /* @__PURE__ */ jsxs2("div", { className: "b316-row", children: [
613
+ /* @__PURE__ */ jsx3("span", { className: "b316-muted", children: "Tax" }),
614
+ /* @__PURE__ */ jsx3("span", { className: "b316-num", children: money(invoice.taxTotal, invoice.currency) })
615
+ ] }) : null,
616
+ /* @__PURE__ */ jsxs2("div", { className: "b316-row b316-invoice-total", children: [
617
+ /* @__PURE__ */ jsx3("span", { children: "Total" }),
618
+ /* @__PURE__ */ jsx3("span", { className: "b316-num", children: money(invoice.total, invoice.currency) })
619
+ ] }),
620
+ Number(invoice.amountPaid) > 0 ? /* @__PURE__ */ jsxs2("div", { className: "b316-row", children: [
621
+ /* @__PURE__ */ jsx3("span", { className: "b316-muted", children: "Paid" }),
622
+ /* @__PURE__ */ jsx3("span", { className: "b316-num", children: money(invoice.amountPaid, invoice.currency) })
623
+ ] }) : null
624
+ ] })
625
+ ] })
626
+ ] });
627
+ }
628
+
629
+ // src/SignIn.tsx
630
+ import {
631
+ isPasskeyAutofillAvailable,
632
+ isPasskeySupported,
633
+ needsSecondFactor
634
+ } from "@base316/sdk";
635
+ import { useEffect as useEffect3, useRef, useState as useState3 } from "react";
636
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
637
+ function SignIn({
638
+ onSignedIn,
639
+ passkeys = true,
640
+ className,
641
+ style
642
+ }) {
643
+ const { client, setToken } = useBase316();
644
+ const [email, setEmail] = useState3("");
645
+ const [password, setPassword] = useState3("");
646
+ const [code, setCode] = useState3("");
647
+ const [pendingUserId, setPendingUserId] = useState3(null);
648
+ const [error, setError] = useState3(null);
649
+ const [busy, setBusy] = useState3(false);
650
+ const [passkeyReady, setPasskeyReady] = useState3(false);
651
+ const abortRef = useRef(null);
652
+ const signInWithPasskey = async (conditional, signal) => {
653
+ if (!conditional) {
654
+ setBusy(true);
655
+ setError(null);
656
+ }
657
+ try {
658
+ const result = await client.auth.passkeys.signIn({ conditional, signal });
659
+ setToken(result.token);
660
+ onSignedIn?.();
661
+ } catch (cause) {
662
+ if (cause instanceof DOMException && (cause.name === "NotAllowedError" || cause.name === "AbortError")) {
663
+ return;
664
+ }
665
+ if (!conditional) {
666
+ console.error("[base316] passkey sign-in failed", cause);
667
+ setError("That passkey did not work.");
668
+ }
669
+ } finally {
670
+ if (!conditional) {
671
+ setBusy(false);
672
+ }
673
+ }
674
+ };
675
+ useEffect3(() => {
676
+ if (!passkeys || !isPasskeySupported() || pendingUserId) {
677
+ return;
678
+ }
679
+ setPasskeyReady(true);
680
+ const controller = new AbortController();
681
+ abortRef.current = controller;
682
+ void isPasskeyAutofillAvailable().then((available) => {
683
+ if (available && !controller.signal.aborted) {
684
+ void signInWithPasskey(true, controller.signal);
685
+ }
686
+ });
687
+ return () => {
688
+ controller.abort();
689
+ };
690
+ }, [passkeys, pendingUserId]);
691
+ async function submit(event) {
692
+ event.preventDefault();
693
+ setBusy(true);
694
+ setError(null);
695
+ try {
696
+ if (pendingUserId) {
697
+ const verified = await client.auth.verifyMfa({ userId: pendingUserId, code });
698
+ setToken(verified.token);
699
+ onSignedIn?.();
700
+ return;
701
+ }
702
+ const result = await client.auth.signIn({ email, password });
703
+ if (needsSecondFactor(result)) {
704
+ setPendingUserId(result.userId);
705
+ return;
706
+ }
707
+ setToken(result.token);
708
+ onSignedIn?.();
709
+ } catch (cause) {
710
+ console.error("[base316] sign-in failed", cause);
711
+ setError(
712
+ pendingUserId ? "That code was not accepted." : "That email and password did not match."
713
+ );
714
+ } finally {
715
+ setBusy(false);
716
+ }
717
+ }
718
+ return /* @__PURE__ */ jsxs3("form", { onSubmit: submit, className: cls("b316-card", className), style, children: [
719
+ error ? /* @__PURE__ */ jsx4("div", { className: "b316-error", children: error }) : null,
720
+ pendingUserId ? /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
721
+ /* @__PURE__ */ jsx4("label", { className: "b316-label", htmlFor: "b316-code", children: "Authentication code" }),
722
+ /* @__PURE__ */ jsx4(
723
+ "input",
724
+ {
725
+ id: "b316-code",
726
+ className: "b316-input",
727
+ value: code,
728
+ onChange: (event) => setCode(event.target.value),
729
+ autoComplete: "one-time-code",
730
+ inputMode: "numeric",
731
+ required: true
732
+ }
733
+ ),
734
+ /* @__PURE__ */ jsx4("span", { className: "b316-muted", children: "From your authenticator app, or one of your recovery codes." })
735
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
736
+ /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
737
+ /* @__PURE__ */ jsx4("label", { className: "b316-label", htmlFor: "b316-email", children: "Email" }),
738
+ /* @__PURE__ */ jsx4(
739
+ "input",
740
+ {
741
+ id: "b316-email",
742
+ className: "b316-input",
743
+ type: "email",
744
+ value: email,
745
+ onChange: (event) => setEmail(event.target.value),
746
+ autoComplete: passkeyReady ? "email webauthn" : "email",
747
+ required: true
748
+ }
749
+ )
750
+ ] }),
751
+ /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
752
+ /* @__PURE__ */ jsx4("label", { className: "b316-label", htmlFor: "b316-password", children: "Password" }),
753
+ /* @__PURE__ */ jsx4(
754
+ "input",
755
+ {
756
+ id: "b316-password",
757
+ className: "b316-input",
758
+ type: "password",
759
+ value: password,
760
+ onChange: (event) => setPassword(event.target.value),
761
+ autoComplete: "current-password",
762
+ required: true
763
+ }
764
+ )
765
+ ] })
766
+ ] }),
767
+ /* @__PURE__ */ jsx4("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Working\u2026" : pendingUserId ? "Verify" : "Sign in" }),
768
+ passkeyReady && !pendingUserId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
769
+ /* @__PURE__ */ jsx4("div", { className: "b316-muted", style: { textAlign: "center", margin: "10px 0" }, children: "or" }),
770
+ /* @__PURE__ */ jsx4(
771
+ "button",
772
+ {
773
+ type: "button",
774
+ className: "b316-button b316-button-secondary",
775
+ disabled: busy,
776
+ onClick: () => {
777
+ abortRef.current?.abort();
778
+ void signInWithPasskey(false);
779
+ },
780
+ children: "Sign in with a passkey"
781
+ }
782
+ )
783
+ ] }) : null
784
+ ] });
785
+ }
786
+
787
+ // src/SignUp.tsx
788
+ import { useState as useState4 } from "react";
789
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
790
+ function SignUp({
791
+ onSignedUp,
792
+ className,
793
+ style
794
+ }) {
795
+ const { client, setToken } = useBase316();
796
+ const [name, setName] = useState4("");
797
+ const [email, setEmail] = useState4("");
798
+ const [password, setPassword] = useState4("");
799
+ const [error, setError] = useState4(null);
800
+ const [busy, setBusy] = useState4(false);
801
+ async function submit(event) {
802
+ event.preventDefault();
803
+ setBusy(true);
804
+ setError(null);
805
+ try {
806
+ await client.auth.signUp({ email, password, name: name || void 0 });
807
+ const signedIn = await client.auth.signIn({ email, password });
808
+ if ("token" in signedIn && signedIn.token) {
809
+ setToken(signedIn.token);
810
+ }
811
+ onSignedUp?.();
812
+ } catch (cause) {
813
+ console.error("[base316] sign-up failed", cause);
814
+ setError("We could not create that account. The email may already be registered.");
815
+ } finally {
816
+ setBusy(false);
817
+ }
818
+ }
819
+ return /* @__PURE__ */ jsxs4("form", { onSubmit: submit, className: cls("b316-card", className), style, children: [
820
+ error ? /* @__PURE__ */ jsx5("div", { className: "b316-error", children: error }) : null,
821
+ /* @__PURE__ */ jsxs4("div", { className: "b316-field", children: [
822
+ /* @__PURE__ */ jsx5("label", { className: "b316-label", htmlFor: "b316-name", children: "Name" }),
823
+ /* @__PURE__ */ jsx5(
824
+ "input",
825
+ {
826
+ id: "b316-name",
827
+ className: "b316-input",
828
+ value: name,
829
+ onChange: (event) => setName(event.target.value),
830
+ autoComplete: "name"
831
+ }
832
+ )
833
+ ] }),
834
+ /* @__PURE__ */ jsxs4("div", { className: "b316-field", children: [
835
+ /* @__PURE__ */ jsx5("label", { className: "b316-label", htmlFor: "b316-signup-email", children: "Email" }),
836
+ /* @__PURE__ */ jsx5(
837
+ "input",
838
+ {
839
+ id: "b316-signup-email",
840
+ className: "b316-input",
841
+ type: "email",
842
+ value: email,
843
+ onChange: (event) => setEmail(event.target.value),
844
+ autoComplete: "email",
845
+ required: true
846
+ }
847
+ )
848
+ ] }),
849
+ /* @__PURE__ */ jsxs4("div", { className: "b316-field", children: [
850
+ /* @__PURE__ */ jsx5("label", { className: "b316-label", htmlFor: "b316-signup-password", children: "Password" }),
851
+ /* @__PURE__ */ jsx5(
852
+ "input",
853
+ {
854
+ id: "b316-signup-password",
855
+ className: "b316-input",
856
+ type: "password",
857
+ value: password,
858
+ onChange: (event) => setPassword(event.target.value),
859
+ autoComplete: "new-password",
860
+ minLength: 8,
861
+ required: true
862
+ }
863
+ ),
864
+ /* @__PURE__ */ jsx5("span", { className: "b316-muted", children: "At least 8 characters." })
865
+ ] }),
866
+ /* @__PURE__ */ jsx5("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Creating\u2026" : "Create account" })
867
+ ] });
868
+ }
869
+
870
+ // src/UserProfile.tsx
871
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
872
+ function UserProfile({ className, style }) {
873
+ const { session, loading, signOut } = useBase316();
874
+ if (loading) {
875
+ return /* @__PURE__ */ jsx6("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
876
+ }
877
+ if (!session) {
878
+ return /* @__PURE__ */ jsx6("div", { className: cls("b316-card", className), style, children: "Not signed in." });
879
+ }
880
+ const user = session.user ?? {};
881
+ return /* @__PURE__ */ jsx6("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsxs5("div", { className: "b316-row", children: [
882
+ /* @__PURE__ */ jsxs5("div", { children: [
883
+ /* @__PURE__ */ jsx6("div", { style: { fontWeight: 600 }, children: user.name || user.email || "Signed in" }),
884
+ user.name && user.email ? /* @__PURE__ */ jsx6("div", { className: "b316-muted", children: user.email }) : null,
885
+ user.emailVerified === false ? /* @__PURE__ */ jsx6("div", { className: "b316-muted", children: "Email not verified" }) : null
886
+ ] }),
887
+ /* @__PURE__ */ jsx6(
888
+ "button",
889
+ {
890
+ type: "button",
891
+ className: "b316-button b316-button-secondary",
892
+ onClick: () => void signOut(),
893
+ children: "Sign out"
894
+ }
895
+ )
896
+ ] }) });
897
+ }
898
+
899
+ // src/PasskeyManager.tsx
900
+ import { isPasskeySupported as isPasskeySupported2 } from "@base316/sdk";
901
+ import { useCallback as useCallback3, useEffect as useEffect4, useState as useState5 } from "react";
902
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
903
+ function PasskeyManager({
904
+ className,
905
+ style,
906
+ title = "Passkeys"
907
+ }) {
908
+ const { client, token, session } = useBase316();
909
+ const [passkeys, setPasskeys] = useState5(null);
910
+ const [busy, setBusy] = useState5(false);
911
+ const [error, setError] = useState5(null);
912
+ const [name, setName] = useState5("");
913
+ const [supported, setSupported] = useState5(false);
914
+ const load = useCallback3(async () => {
915
+ if (!token) {
916
+ return;
917
+ }
918
+ try {
919
+ const result = await client.auth.passkeys.list(token);
920
+ setPasskeys(result.data);
921
+ } catch {
922
+ setPasskeys([]);
923
+ }
924
+ }, [client, token]);
925
+ useEffect4(() => {
926
+ if (!isPasskeySupported2()) {
927
+ return;
928
+ }
929
+ setSupported(true);
930
+ void load();
931
+ }, [load]);
932
+ if (!session || !token || !supported) {
933
+ return null;
934
+ }
935
+ const add = async () => {
936
+ setBusy(true);
937
+ setError(null);
938
+ try {
939
+ await client.auth.passkeys.register(token, { name: name.trim() || void 0 });
940
+ setName("");
941
+ await load();
942
+ } catch (caught) {
943
+ if (caught instanceof DOMException && caught.name === "NotAllowedError") {
944
+ setError(null);
945
+ } else {
946
+ setError(caught instanceof Error ? caught.message : "That did not work.");
947
+ }
948
+ } finally {
949
+ setBusy(false);
950
+ }
951
+ };
952
+ const remove = async (id) => {
953
+ setError(null);
954
+ try {
955
+ await client.auth.passkeys.remove(token, id);
956
+ await load();
957
+ } catch (caught) {
958
+ setError(caught instanceof Error ? caught.message : "That did not work.");
959
+ }
960
+ };
961
+ return /* @__PURE__ */ jsxs6("div", { className: cls("b316-card", className), style, children: [
962
+ /* @__PURE__ */ jsx7("div", { style: { fontWeight: 600, marginBottom: 4 }, children: title }),
963
+ /* @__PURE__ */ jsx7("div", { className: "b316-muted", style: { marginBottom: 12 }, children: "Sign in with your fingerprint, face or a security key \u2014 nothing to type and nothing to remember." }),
964
+ passkeys === null ? /* @__PURE__ */ jsx7("div", { className: "b316-muted", children: "Loading\u2026" }) : passkeys.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "b316-muted", children: "No passkeys yet." }) : /* @__PURE__ */ jsx7("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: passkeys.map((passkey) => /* @__PURE__ */ jsxs6("li", { className: "b316-row", style: { padding: "8px 0" }, children: [
965
+ /* @__PURE__ */ jsxs6("div", { children: [
966
+ /* @__PURE__ */ jsx7("div", { children: passkey.name || "Passkey" }),
967
+ /* @__PURE__ */ jsxs6("div", { className: "b316-muted", children: [
968
+ passkey.lastUsedAt ? `Last used ${formatDate(passkey.lastUsedAt)}` : `Added ${formatDate(passkey.createdAt)}`,
969
+ passkey.backedUp ? "" : " \xB7 this device only"
970
+ ] })
971
+ ] }),
972
+ /* @__PURE__ */ jsx7(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ className: "b316-button b316-button-secondary",
977
+ onClick: () => void remove(passkey.id),
978
+ children: "Remove"
979
+ }
980
+ )
981
+ ] }, passkey.id)) }),
982
+ error ? /* @__PURE__ */ jsx7("div", { className: "b316-error", style: { marginTop: 12 }, children: error }) : null,
983
+ /* @__PURE__ */ jsxs6("div", { className: "b316-row", style: { marginTop: 12, gap: 8 }, children: [
984
+ /* @__PURE__ */ jsx7(
985
+ "input",
986
+ {
987
+ className: "b316-input",
988
+ placeholder: "Name this passkey (optional)",
989
+ value: name,
990
+ onChange: (event) => setName(event.target.value)
991
+ }
992
+ ),
993
+ /* @__PURE__ */ jsx7(
994
+ "button",
995
+ {
996
+ type: "button",
997
+ className: "b316-button",
998
+ disabled: busy,
999
+ onClick: () => void add(),
1000
+ children: busy ? "Waiting\u2026" : "Add a passkey"
1001
+ }
1002
+ )
1003
+ ] })
1004
+ ] });
1005
+ }
1006
+ function formatDate(value) {
1007
+ const date = new Date(value);
1008
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleDateString();
1009
+ }
1010
+
1011
+ // src/PricingTable.tsx
1012
+ import { useEffect as useEffect5, useState as useState6 } from "react";
1013
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1014
+ function formatAmount(amount, currency) {
1015
+ if (amount === null) return "Free";
1016
+ const value = Number(amount);
1017
+ if (!Number.isFinite(value)) return "Free";
1018
+ try {
1019
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(value);
1020
+ } catch {
1021
+ return `${value} ${currency}`;
1022
+ }
1023
+ }
1024
+ function cadence(plan, amount) {
1025
+ if (amount === null) {
1026
+ return "\xA0";
1027
+ }
1028
+ const unit = plan.interval.toLowerCase();
1029
+ return plan.intervalCount === 1 ? `per ${unit}` : `every ${plan.intervalCount} ${unit}s`;
1030
+ }
1031
+ function PricingTable({
1032
+ onSelectPlan,
1033
+ currentPlanVersionId,
1034
+ featuredPlanKey,
1035
+ className,
1036
+ style
1037
+ }) {
1038
+ const { client } = useBase316();
1039
+ const [plans, setPlans] = useState6(null);
1040
+ const [error, setError] = useState6(false);
1041
+ const [busy, setBusy] = useState6(null);
1042
+ useEffect5(() => {
1043
+ let cancelled = false;
1044
+ client.billing.plans().then((result) => {
1045
+ if (!cancelled) setPlans(result.data);
1046
+ }).catch((cause) => {
1047
+ console.error("[base316] could not load plans", cause);
1048
+ if (!cancelled) setError(true);
1049
+ });
1050
+ return () => {
1051
+ cancelled = true;
1052
+ };
1053
+ }, [client]);
1054
+ if (error) {
1055
+ return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "Plans are unavailable right now." });
1056
+ }
1057
+ if (!plans) {
1058
+ return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "Loading plans\u2026" });
1059
+ }
1060
+ if (plans.length === 0) {
1061
+ return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "No plans are published yet." });
1062
+ }
1063
+ const anyTrial = plans.some((plan) => Boolean(plan.trialDays));
1064
+ const anyDescription = plans.some((plan) => Boolean(plan.description));
1065
+ const anyFeatured = plans.some((plan) => plan.key === featuredPlanKey);
1066
+ return /* @__PURE__ */ jsx8("div", { className: cls("b316-plans", className), style, children: plans.map((plan) => {
1067
+ const base = plan.prices.find((price) => price.featureId === null) ?? plan.prices[0];
1068
+ const current = currentPlanVersionId === plan.planVersionId;
1069
+ const featured = featuredPlanKey === plan.key;
1070
+ return /* @__PURE__ */ jsxs7(
1071
+ "div",
1072
+ {
1073
+ className: featured ? "b316-card b316-plan b316-plan-featured" : "b316-card b316-plan",
1074
+ children: [
1075
+ /* @__PURE__ */ jsxs7("div", { className: "b316-plan-head", children: [
1076
+ anyFeatured ? /* @__PURE__ */ jsx8("div", { className: featured ? "b316-plan-badge" : "b316-plan-badge-spacer", children: featured ? "Recommended" : "\xA0" }) : null,
1077
+ /* @__PURE__ */ jsx8("div", { className: "b316-plan-name", children: plan.name }),
1078
+ anyDescription ? /* @__PURE__ */ jsx8("div", { className: "b316-plan-desc", children: plan.description ?? "\xA0" }) : null
1079
+ ] }),
1080
+ /* @__PURE__ */ jsxs7("div", { className: "b316-plan-pricing", children: [
1081
+ /* @__PURE__ */ jsx8("div", { className: "b316-plan-price", children: formatAmount(base?.amount ?? null, plan.currency) }),
1082
+ /* @__PURE__ */ jsx8("div", { className: "b316-plan-cadence", children: cadence(plan, base?.amount ?? null) }),
1083
+ anyTrial ? /* @__PURE__ */ jsx8("div", { className: "b316-plan-trial", children: plan.trialDays ? `${plan.trialDays}-day free trial` : "\xA0" }) : null
1084
+ ] }),
1085
+ /* @__PURE__ */ jsx8("ul", { className: "b316-plan-features", children: plan.entitlements.filter((entitlement) => entitlement.enabled).map((entitlement) => /* @__PURE__ */ jsxs7("li", { className: "b316-plan-feature", children: [
1086
+ /* @__PURE__ */ jsx8("span", { children: entitlement.featureName }),
1087
+ /* @__PURE__ */ jsx8("span", { className: "b316-plan-feature-value", children: entitlement.limit === null ? "Unlimited" : entitlement.limit })
1088
+ ] }, entitlement.featureId)) }),
1089
+ /* @__PURE__ */ jsx8("div", { className: "b316-plan-action", children: /* @__PURE__ */ jsx8(
1090
+ "button",
1091
+ {
1092
+ type: "button",
1093
+ className: current ? "b316-button b316-button-current" : "b316-button",
1094
+ disabled: current || busy !== null,
1095
+ onClick: async () => {
1096
+ setBusy(plan.planVersionId);
1097
+ try {
1098
+ await onSelectPlan(plan.planVersionId, plan);
1099
+ } finally {
1100
+ setBusy(null);
1101
+ }
1102
+ },
1103
+ children: current ? "Current plan" : busy === plan.planVersionId ? "Working\u2026" : "Choose"
1104
+ }
1105
+ ) })
1106
+ ]
1107
+ },
1108
+ plan.id
1109
+ );
1110
+ }) });
1111
+ }
1112
+
1113
+ // src/SubscriptionManager.tsx
1114
+ import { useCallback as useCallback4, useEffect as useEffect6, useState as useState7 } from "react";
1115
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1116
+ function statusLabel(status) {
1117
+ const words = status.toLowerCase().split("_");
1118
+ return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
1119
+ }
1120
+ function SubscriptionManager({
1121
+ className,
1122
+ style
1123
+ }) {
1124
+ const { client, token, loading: sessionLoading } = useBase316();
1125
+ const [subscription, setSubscription] = useState7(null);
1126
+ const [loading, setLoading] = useState7(true);
1127
+ const [busy, setBusy] = useState7(false);
1128
+ const [error, setError] = useState7(null);
1129
+ const load = useCallback4(async () => {
1130
+ if (!token) {
1131
+ setSubscription(null);
1132
+ setLoading(false);
1133
+ return;
1134
+ }
1135
+ try {
1136
+ const result = await client.billing.subscription(token);
1137
+ setSubscription(result.subscription);
1138
+ } catch (cause) {
1139
+ console.error("[base316] could not load the subscription", cause);
1140
+ setError("Could not load your subscription.");
1141
+ } finally {
1142
+ setLoading(false);
1143
+ }
1144
+ }, [client, token]);
1145
+ useEffect6(() => {
1146
+ void load();
1147
+ }, [load]);
1148
+ if (sessionLoading || loading) {
1149
+ return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
1150
+ }
1151
+ if (!token) {
1152
+ return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "Sign in to manage your subscription." });
1153
+ }
1154
+ if (!subscription) {
1155
+ return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "You are not subscribed to a plan." });
1156
+ }
1157
+ const renews = new Date(subscription.currentPeriodEnd).toLocaleDateString();
1158
+ return /* @__PURE__ */ jsxs8("div", { className: cls("b316-card", className), style, children: [
1159
+ error ? /* @__PURE__ */ jsx9("div", { className: "b316-error", children: error }) : null,
1160
+ /* @__PURE__ */ jsx9("div", { className: "b316-plan-name", children: subscription.planVersion.plan.name }),
1161
+ /* @__PURE__ */ jsxs8("div", { className: "b316-muted", children: [
1162
+ statusLabel(subscription.status),
1163
+ " \xB7 ",
1164
+ subscription.cancelAtPeriodEnd ? `Ends ${renews}` : `Renews ${renews}`
1165
+ ] }),
1166
+ subscription.cancelAtPeriodEnd ? /* @__PURE__ */ jsxs8("p", { className: "b316-muted", children: [
1167
+ "This subscription will not renew. You keep access until ",
1168
+ renews,
1169
+ "."
1170
+ ] }) : /* @__PURE__ */ jsx9(
1171
+ "button",
1172
+ {
1173
+ type: "button",
1174
+ className: "b316-button b316-button-secondary",
1175
+ style: { marginTop: 14 },
1176
+ disabled: busy,
1177
+ onClick: async () => {
1178
+ setBusy(true);
1179
+ setError(null);
1180
+ try {
1181
+ await client.billing.cancel(token);
1182
+ await load();
1183
+ } catch (cause) {
1184
+ console.error("[base316] cancel failed", cause);
1185
+ setError("Could not cancel. Try again.");
1186
+ } finally {
1187
+ setBusy(false);
1188
+ }
1189
+ },
1190
+ children: busy ? "Working\u2026" : "Cancel subscription"
1191
+ }
1192
+ )
1193
+ ] });
1194
+ }
1195
+ export {
1196
+ Base316Provider,
1197
+ Branding,
1198
+ Invoices,
1199
+ PasskeyManager,
1200
+ PricingTable,
1201
+ SignIn,
1202
+ SignUp,
1203
+ SubscriptionManager,
1204
+ UserProfile,
1205
+ useBase316,
1206
+ useSession
1207
+ };
1208
+ //# sourceMappingURL=index.js.map