@lumnsh/react 0.1.1

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,697 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ LumnCheckout: () => LumnCheckout,
24
+ LumnGate: () => LumnGate,
25
+ LumnPricing: () => LumnPricing,
26
+ LumnProvider: () => LumnProvider,
27
+ formatIntervalPrice: () => formatIntervalPrice,
28
+ useEntitlements: () => useEntitlements,
29
+ useLumn: () => useLumn,
30
+ useLumnContext: () => useLumnContext,
31
+ usePricing: () => usePricing,
32
+ useSubscriptions: () => useSubscriptions
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/components/LumnProvider.tsx
37
+ var import_react = require("react");
38
+ var import_node = require("@lumnsh/node");
39
+ var import_jsx_runtime = require("react/jsx-runtime");
40
+ var ENV_TO_INTERNAL = {
41
+ sandbox: "development",
42
+ live: "production"
43
+ };
44
+ var LumnContext = (0, import_react.createContext)(null);
45
+ var THEME_VARS_DARK = {
46
+ ["--lumn-bg"]: "hsl(220 13% 8%)",
47
+ ["--lumn-bg-elevated"]: "hsl(220 13% 12%)",
48
+ ["--lumn-text"]: "hsl(0 0% 98%)",
49
+ ["--lumn-text-muted"]: "hsl(215 10% 75%)",
50
+ ["--lumn-border"]: "hsl(220 10% 18%)",
51
+ ["--lumn-border-light"]: "hsl(220 10% 15%)",
52
+ ["--lumn-error"]: "hsl(0 63% 50%)"
53
+ };
54
+ var THEME_VARS_LIGHT = {
55
+ ["--lumn-bg"]: "hsl(0 0% 98%)",
56
+ ["--lumn-bg-elevated"]: "hsl(0 0% 96%)",
57
+ ["--lumn-text"]: "hsl(220 13% 8%)",
58
+ ["--lumn-text-muted"]: "hsl(220 10% 45%)",
59
+ ["--lumn-border"]: "hsl(220 10% 85%)",
60
+ ["--lumn-border-light"]: "hsl(220 10% 75%)",
61
+ ["--lumn-error"]: "hsl(0 63% 45%)"
62
+ };
63
+ var DEFAULT_BASE_URL = "https://app.lumn.dev";
64
+ var DEFAULT_ACCENT = "#22c55e";
65
+ function LumnProvider({
66
+ apiKey,
67
+ baseUrl = DEFAULT_BASE_URL,
68
+ environment,
69
+ accentColor = DEFAULT_ACCENT,
70
+ theme = "dark",
71
+ children
72
+ }) {
73
+ const lumn = (0, import_react.useMemo)(() => new import_node.Lumn({ apiKey, baseUrl }), [apiKey, baseUrl]);
74
+ const value = (0, import_react.useMemo)(
75
+ () => ({
76
+ lumn,
77
+ environment: environment ? ENV_TO_INTERNAL[environment] : void 0,
78
+ accentColor,
79
+ theme
80
+ }),
81
+ [lumn, environment, accentColor, theme]
82
+ );
83
+ const themeVars = theme === "light" ? THEME_VARS_LIGHT : THEME_VARS_DARK;
84
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LumnContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: themeVars, children }) });
85
+ }
86
+ function useLumnContext() {
87
+ const ctx = (0, import_react.useContext)(LumnContext);
88
+ if (!ctx) {
89
+ throw new Error("useLumnContext must be used within LumnProvider");
90
+ }
91
+ return ctx;
92
+ }
93
+
94
+ // src/components/LumnCheckout.tsx
95
+ var import_react2 = require("react");
96
+ var import_stripe_js = require("@stripe/stripe-js");
97
+ var import_react_stripe_js = require("@stripe/react-stripe-js");
98
+ var import_jsx_runtime2 = require("react/jsx-runtime");
99
+ var stripePromiseCache = /* @__PURE__ */ new Map();
100
+ function getStripePromise(publishableKey) {
101
+ let p = stripePromiseCache.get(publishableKey);
102
+ if (!p) {
103
+ p = (0, import_stripe_js.loadStripe)(publishableKey);
104
+ stripePromiseCache.set(publishableKey, p);
105
+ }
106
+ return p;
107
+ }
108
+ function LumnCheckout({
109
+ customerId,
110
+ productSlug,
111
+ priceInterval = "monthly",
112
+ branding,
113
+ onComplete,
114
+ className = ""
115
+ }) {
116
+ const { lumn } = useLumnContext();
117
+ const [session, setSession] = (0, import_react2.useState)(null);
118
+ const [error, setError] = (0, import_react2.useState)(null);
119
+ const brandingKey = JSON.stringify(branding ?? {});
120
+ (0, import_react2.useEffect)(() => {
121
+ if (!customerId || !productSlug) return;
122
+ let cancelled = false;
123
+ setError(null);
124
+ setSession(null);
125
+ const returnUrl = typeof window !== "undefined" ? window.location.href : "";
126
+ const brandingPayload = branding && Object.keys(branding).length > 0 ? branding : void 0;
127
+ lumn.checkout.create({
128
+ customer_id: customerId,
129
+ product_slug: productSlug,
130
+ price_interval: priceInterval,
131
+ ui_mode: "embedded",
132
+ return_url: returnUrl,
133
+ ...brandingPayload && { branding_settings: brandingPayload }
134
+ }).then((d) => {
135
+ if (cancelled) return;
136
+ if (!d?.publishable_key) {
137
+ throw new Error("Missing publishable_key \u2014 configure a publishable key on your Stripe connection");
138
+ }
139
+ if (!d?.client_secret) {
140
+ throw new Error("Missing client_secret from checkout session");
141
+ }
142
+ setSession({
143
+ clientSecret: d.client_secret,
144
+ publishableKey: d.publishable_key
145
+ });
146
+ }).catch((e) => {
147
+ if (!cancelled) {
148
+ setError(e instanceof Error ? e.message : "Checkout failed");
149
+ }
150
+ });
151
+ return () => {
152
+ cancelled = true;
153
+ };
154
+ }, [lumn, customerId, productSlug, priceInterval, brandingKey]);
155
+ const stripePromise = session?.publishableKey ? getStripePromise(session.publishableKey) : null;
156
+ const fetchClientSecret = (0, import_react2.useMemo)(() => {
157
+ const secret = session?.clientSecret?.trim();
158
+ if (!secret) return void 0;
159
+ return () => Promise.resolve(secret);
160
+ }, [session?.clientSecret]);
161
+ if (!customerId || !productSlug) {
162
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
163
+ "div",
164
+ {
165
+ className,
166
+ style: { color: "#ef4444", fontSize: "0.875rem" },
167
+ children: "customerId and productSlug are required"
168
+ }
169
+ );
170
+ }
171
+ if (error) {
172
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
173
+ "div",
174
+ {
175
+ className,
176
+ style: { color: "#ef4444", fontSize: "0.875rem" },
177
+ children: error
178
+ }
179
+ );
180
+ }
181
+ if (!session || !stripePromise || !fetchClientSecret) {
182
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
183
+ "div",
184
+ {
185
+ className,
186
+ style: { color: "#737373", fontSize: "0.875rem" },
187
+ children: "Loading checkout\u2026"
188
+ }
189
+ );
190
+ }
191
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
192
+ import_react_stripe_js.EmbeddedCheckoutProvider,
193
+ {
194
+ stripe: stripePromise,
195
+ options: {
196
+ fetchClientSecret,
197
+ onComplete: onComplete ?? void 0
198
+ },
199
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_stripe_js.EmbeddedCheckout, {})
200
+ }
201
+ ) });
202
+ }
203
+
204
+ // src/components/LumnPricing.tsx
205
+ var import_react4 = require("react");
206
+
207
+ // src/hooks/usePricing.ts
208
+ var import_react3 = require("react");
209
+ function formatIntervalPrice(plan, _interval, options) {
210
+ const locale = options?.locale ?? "en-US";
211
+ const opts = { style: "currency", currency: plan.currency || "usd" };
212
+ if (plan.tiers && plan.tiers.length > 0) {
213
+ const first = plan.tiers[0];
214
+ const amt = first?.unitAmount ?? 0;
215
+ if (amt === 0) return "From Free";
216
+ return `From ${(amt / 100).toLocaleString(locale, opts)}`;
217
+ }
218
+ if (plan.amount != null) {
219
+ if (plan.amount === 0) return "Free";
220
+ return (plan.amount / 100).toLocaleString(locale, opts);
221
+ }
222
+ return "Custom";
223
+ }
224
+ function usePricing(options) {
225
+ const { lumn } = useLumnContext();
226
+ const [products, setProducts] = (0, import_react3.useState)([]);
227
+ const [isLoading, setIsLoading] = (0, import_react3.useState)(!options?.skip);
228
+ const [error, setError] = (0, import_react3.useState)(null);
229
+ const fetchProducts = (0, import_react3.useCallback)(async () => {
230
+ setIsLoading(true);
231
+ setError(null);
232
+ try {
233
+ const result = await lumn.products.list({ limit: 50, active: true });
234
+ setProducts(result.data ?? []);
235
+ } catch (e) {
236
+ setError(e instanceof Error ? e : new Error("Failed to fetch products"));
237
+ setProducts([]);
238
+ } finally {
239
+ setIsLoading(false);
240
+ }
241
+ }, [lumn]);
242
+ (0, import_react3.useEffect)(() => {
243
+ if (options?.skip) return;
244
+ fetchProducts();
245
+ }, [fetchProducts, options?.skip]);
246
+ return { products, isLoading: options?.skip ? false : isLoading, error: options?.skip ? null : error, refetch: fetchProducts };
247
+ }
248
+
249
+ // src/components/IntervalSwitcher.tsx
250
+ var import_jsx_runtime3 = require("react/jsx-runtime");
251
+ function IntervalSwitcher({
252
+ intervals,
253
+ selected,
254
+ onSelect,
255
+ accentColor = "#22c55e"
256
+ }) {
257
+ if (intervals.length <= 1) return null;
258
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: "1rem", display: "flex", gap: "0.5rem" }, children: intervals.map((iv) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
259
+ "button",
260
+ {
261
+ type: "button",
262
+ onClick: () => onSelect(iv),
263
+ style: {
264
+ padding: "0.375rem 0.75rem",
265
+ fontSize: "0.875rem",
266
+ fontFamily: "Inter, system-ui, sans-serif",
267
+ border: selected === iv ? `1px solid ${accentColor}` : "1px solid var(--lumn-border-light, hsl(220 10% 15%))",
268
+ borderRadius: "0.5rem",
269
+ background: selected === iv ? `${accentColor}1a` : "var(--lumn-bg-elevated, hsl(220 13% 12%))",
270
+ color: selected === iv ? accentColor : "var(--lumn-text, hsl(0 0% 98%))",
271
+ cursor: "pointer"
272
+ },
273
+ children: iv.charAt(0).toUpperCase() + iv.slice(1)
274
+ },
275
+ iv
276
+ )) });
277
+ }
278
+
279
+ // src/components/PricingCard.tsx
280
+ var import_jsx_runtime4 = require("react/jsx-runtime");
281
+ function formatTierRange(tierIndex, tiers) {
282
+ const tier = tiers[tierIndex];
283
+ const prevUpTo = tierIndex === 0 ? 0 : tiers[tierIndex - 1].upTo ?? 0;
284
+ if (tier.upTo === null) return `${prevUpTo + 1}+`;
285
+ if (prevUpTo === 0) return `1\u2013${tier.upTo}`;
286
+ return `${prevUpTo + 1}\u2013${tier.upTo}`;
287
+ }
288
+ function PricingCard({
289
+ product,
290
+ priceInterval,
291
+ plan,
292
+ priceStr,
293
+ period,
294
+ trialDays,
295
+ isCheckoutLoading,
296
+ checkoutError,
297
+ cardStyle,
298
+ accentColor,
299
+ locale,
300
+ canSubscribe,
301
+ onCtaClick
302
+ }) {
303
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
304
+ "div",
305
+ {
306
+ style: {
307
+ background: "var(--lumn-bg, hsl(220 13% 8%))",
308
+ border: `1px solid ${cardStyle.borderColor}`,
309
+ borderRadius: "0.5rem",
310
+ padding: "1.5rem",
311
+ display: "flex",
312
+ flexDirection: "column",
313
+ boxShadow: "0 0 20px hsl(90 100% 50% / 0.05)"
314
+ },
315
+ children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.5rem" }, children: [
317
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, fontSize: "1.125rem", color: "var(--lumn-text, hsl(0 0% 98%))" }, children: product.name }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
319
+ "span",
320
+ {
321
+ style: {
322
+ fontSize: "0.65rem",
323
+ fontWeight: 600,
324
+ textTransform: "uppercase",
325
+ letterSpacing: "0.05em",
326
+ padding: "0.2rem 0.5rem",
327
+ borderRadius: "0.25rem",
328
+ background: cardStyle.badgeBg,
329
+ color: cardStyle.badgeColor
330
+ },
331
+ children: product.type ?? "recurring"
332
+ }
333
+ )
334
+ ] }),
335
+ product.description && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginBottom: "1rem", fontSize: "0.875rem", color: "var(--lumn-text-muted, hsl(215 10% 75%))" }, children: product.description }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginTop: "auto", display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
337
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: "1.5rem", fontWeight: 600, color: "var(--lumn-text, hsl(0 0% 98%))" }, children: [
338
+ priceStr,
339
+ period && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "0.875rem", fontWeight: 400, color: "var(--lumn-text-muted, hsl(215 10% 75%))" }, children: period })
340
+ ] }),
341
+ plan?.tiers && plan.tiers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
342
+ "div",
343
+ {
344
+ style: {
345
+ marginTop: "0.25rem",
346
+ padding: "0.5rem 0.75rem",
347
+ borderRadius: "0.375rem",
348
+ background: "var(--lumn-bg-elevated, hsl(220 13% 12%))",
349
+ border: "1px solid var(--lumn-border, hsl(220 10% 18%))"
350
+ },
351
+ children: [
352
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "0.65rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--lumn-text-muted, hsl(215 10% 65%))", marginBottom: "0.375rem" }, children: "Tiers" }),
353
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 0 }, children: plan.tiers.map((tier, i) => {
354
+ const range = formatTierRange(i, plan.tiers);
355
+ const currency = plan.currency ?? "usd";
356
+ const isFree = tier.unitAmount === 0;
357
+ const perUnit = isFree ? "Free" : (tier.unitAmount / 100).toLocaleString(locale ?? "en-US", { style: "currency", currency });
358
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
359
+ "div",
360
+ {
361
+ style: {
362
+ display: "flex",
363
+ justifyContent: "space-between",
364
+ alignItems: "center",
365
+ padding: "0.25rem 0",
366
+ fontSize: "0.8125rem",
367
+ color: "var(--lumn-text-muted, hsl(215 10% 75%))",
368
+ borderBottom: i < plan.tiers.length - 1 ? "1px solid var(--lumn-border, hsl(220 10% 18%))" : "none"
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontFamily: "JetBrains Mono, monospace" }, children: range }),
372
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { fontWeight: 500, color: "var(--lumn-text, hsl(0 0% 92%))" }, children: [
373
+ perUnit,
374
+ !isFree && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontWeight: 400, color: "var(--lumn-text-muted, hsl(215 10% 65%))", fontSize: "0.75rem" }, children: "/unit" })
375
+ ] })
376
+ ]
377
+ },
378
+ i
379
+ );
380
+ }) })
381
+ ]
382
+ }
383
+ ),
384
+ trialDays != null && trialDays > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: "0.75rem", color: accentColor }, children: [
385
+ trialDays,
386
+ "-day free trial"
387
+ ] }),
388
+ canSubscribe ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
389
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
390
+ "button",
391
+ {
392
+ type: "button",
393
+ onClick: () => priceInterval && onCtaClick(product, priceInterval),
394
+ disabled: !!isCheckoutLoading || !priceInterval,
395
+ style: {
396
+ padding: "0.5rem 1rem",
397
+ fontSize: "0.875rem",
398
+ fontWeight: 500,
399
+ fontFamily: "Inter, system-ui, sans-serif",
400
+ border: `1px solid ${accentColor}`,
401
+ borderRadius: "0.5rem",
402
+ background: `${accentColor}1a`,
403
+ color: accentColor,
404
+ cursor: isCheckoutLoading ? "wait" : "pointer"
405
+ },
406
+ children: isCheckoutLoading ? "Loading\u2026" : "Subscribe"
407
+ }
408
+ ),
409
+ checkoutError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "0.75rem", color: "var(--lumn-error, hsl(0 63% 50%))" }, children: checkoutError })
410
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "0.75rem", color: "var(--lumn-text-muted, hsl(215 10% 75%))" }, children: "Sign in to subscribe" })
411
+ ] })
412
+ ]
413
+ }
414
+ );
415
+ }
416
+
417
+ // src/components/LumnPricing.tsx
418
+ var import_jsx_runtime5 = require("react/jsx-runtime");
419
+ var DEFAULT_INTERVALS = ["monthly", "yearly"];
420
+ var intervalPeriod = {
421
+ monthly: "/month",
422
+ quarterly: "/quarter",
423
+ yearly: "/year"
424
+ };
425
+ var CARD_STYLE_BY_TYPE = {
426
+ recurring: { borderColor: "hsl(90 100% 50% / 0.5)", badgeBg: "hsl(90 100% 50% / 0.15)", badgeColor: "hsl(90 100% 55%)" },
427
+ one_time: { borderColor: "hsl(199 89% 48% / 0.5)", badgeBg: "hsl(199 89% 48% / 0.15)", badgeColor: "hsl(199 89% 55%)" },
428
+ usage_based: { borderColor: "hsl(45 93% 47% / 0.5)", badgeBg: "hsl(45 93% 47% / 0.15)", badgeColor: "hsl(45 93% 55%)" },
429
+ seat_based: { borderColor: "hsl(280 65% 60% / 0.5)", badgeBg: "hsl(280 65% 60% / 0.15)", badgeColor: "hsl(280 65% 70%)" },
430
+ tiered: { borderColor: "hsl(340 75% 55% / 0.5)", badgeBg: "hsl(340 75% 55% / 0.15)", badgeColor: "hsl(340 75% 65%)" }
431
+ };
432
+ function getCardStyle(type) {
433
+ const key = type ?? "recurring";
434
+ return CARD_STYLE_BY_TYPE[key] ?? CARD_STYLE_BY_TYPE.recurring;
435
+ }
436
+ function getPriceInterval(product, selectedInterval, intervals) {
437
+ const idx = intervals.indexOf(selectedInterval);
438
+ if (idx === -1) return void 0;
439
+ for (let i = idx; i >= 0; i--) {
440
+ const iv = intervals[i];
441
+ if (product.pricing?.[iv]) return iv;
442
+ }
443
+ return void 0;
444
+ }
445
+ function LumnPricing({
446
+ customerId,
447
+ products: productsProp,
448
+ intervals = DEFAULT_INTERVALS,
449
+ defaultInterval = "monthly",
450
+ locale,
451
+ onSelectPlan,
452
+ renderCard,
453
+ formatPrice,
454
+ loadingComponent,
455
+ emptyComponent,
456
+ errorComponent,
457
+ className = ""
458
+ }) {
459
+ const { lumn, accentColor = "#22c55e" } = useLumnContext();
460
+ const priceFormatter = formatPrice ?? ((plan, interval) => formatIntervalPrice(plan, interval, locale ? { locale } : void 0));
461
+ const { products: fetchedProducts, isLoading, error } = usePricing({ skip: productsProp !== void 0 });
462
+ const products = productsProp ?? fetchedProducts;
463
+ const [selectedInterval, setSelectedInterval] = (0, import_react4.useState)(defaultInterval);
464
+ const [checkoutLoading, setCheckoutLoading] = (0, import_react4.useState)(null);
465
+ const [checkoutErrorBySlug, setCheckoutErrorBySlug] = (0, import_react4.useState)({});
466
+ const createCheckout = (0, import_react4.useCallback)(
467
+ async (productSlug, priceInterval) => {
468
+ if (!customerId) return;
469
+ const fallbackUrl = typeof window !== "undefined" ? window.location.href : "";
470
+ const session = await lumn.checkout.create({
471
+ customer_id: customerId,
472
+ product_slug: productSlug,
473
+ price_interval: priceInterval,
474
+ ui_mode: "hosted",
475
+ success_url: fallbackUrl,
476
+ cancel_url: fallbackUrl
477
+ });
478
+ const checkoutUrl = session?.checkout_url;
479
+ if (checkoutUrl && typeof window !== "undefined") {
480
+ window.location.href = checkoutUrl;
481
+ }
482
+ },
483
+ [lumn, customerId]
484
+ );
485
+ const handleCtaClick = (0, import_react4.useCallback)(
486
+ async (product, interval) => {
487
+ if (!customerId) return;
488
+ const plan = product.pricing?.[interval];
489
+ if (!plan) return;
490
+ const checkout = () => {
491
+ setCheckoutLoading(product.slug);
492
+ setCheckoutErrorBySlug((prev) => ({ ...prev, [product.slug]: "" }));
493
+ createCheckout(product.slug, interval).catch((err) => {
494
+ const msg = err instanceof Error ? err.message : "Checkout failed";
495
+ setCheckoutErrorBySlug((prev) => ({ ...prev, [product.slug]: msg }));
496
+ }).finally(() => setCheckoutLoading(null));
497
+ };
498
+ if (onSelectPlan) {
499
+ onSelectPlan({ product, interval, checkout });
500
+ } else {
501
+ checkout();
502
+ }
503
+ },
504
+ [customerId, createCheckout, onSelectPlan]
505
+ );
506
+ const filteredProducts = products.filter((p) => p.active);
507
+ const effectiveInterval = intervals.includes(selectedInterval) ? selectedInterval : intervals[0];
508
+ if (isLoading && !productsProp?.length) {
509
+ if (loadingComponent) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, children: loadingComponent });
510
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, style: { padding: "1.5rem", color: "var(--lumn-text-muted, hsl(215 10% 75%))", fontSize: "0.875rem", fontFamily: "Inter, system-ui, sans-serif" }, children: "Loading\u2026" });
511
+ }
512
+ if (error && !productsProp?.length) {
513
+ if (errorComponent) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, children: errorComponent(error) });
514
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, style: { padding: "1.5rem", color: "var(--lumn-error, hsl(0 63% 50%))", fontSize: "0.875rem", fontFamily: "Inter, system-ui, sans-serif" }, children: error?.message ?? "Failed to load products" });
515
+ }
516
+ if (filteredProducts.length === 0) {
517
+ if (emptyComponent) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, children: emptyComponent });
518
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, style: { padding: "1.5rem", color: "var(--lumn-text-muted, hsl(215 10% 75%))", fontSize: "0.875rem", fontFamily: "Inter, system-ui, sans-serif" }, children: "No products available." });
519
+ }
520
+ if (renderCard) {
521
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className, children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
523
+ IntervalSwitcher,
524
+ {
525
+ intervals,
526
+ selected: selectedInterval,
527
+ onSelect: setSelectedInterval,
528
+ accentColor
529
+ }
530
+ ),
531
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "1.5rem" }, children: filteredProducts.map((p) => renderCard(p, effectiveInterval)) })
532
+ ] });
533
+ }
534
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className, style: { fontFamily: "Inter, system-ui, sans-serif" }, children: [
535
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
536
+ IntervalSwitcher,
537
+ {
538
+ intervals,
539
+ selected: selectedInterval,
540
+ onSelect: setSelectedInterval,
541
+ accentColor
542
+ }
543
+ ),
544
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "1.5rem" }, children: filteredProducts.map((product) => {
545
+ const priceInterval = getPriceInterval(product, effectiveInterval, intervals);
546
+ const plan = priceInterval ? product.pricing?.[priceInterval] : void 0;
547
+ const priceStr = plan && priceInterval ? priceFormatter(plan, priceInterval) : "Custom";
548
+ const period = priceInterval ? intervalPeriod[priceInterval] : "";
549
+ const trialDays = product.pricing?.trial?.days;
550
+ const isCheckoutLoading = checkoutLoading === product.slug;
551
+ const checkoutError = checkoutErrorBySlug[product.slug];
552
+ const cardStyle = getCardStyle(product.type);
553
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
554
+ PricingCard,
555
+ {
556
+ product,
557
+ priceInterval,
558
+ plan,
559
+ priceStr,
560
+ period,
561
+ trialDays,
562
+ isCheckoutLoading: !!isCheckoutLoading,
563
+ checkoutError,
564
+ cardStyle,
565
+ accentColor,
566
+ locale,
567
+ canSubscribe: !!customerId,
568
+ onCtaClick: handleCtaClick
569
+ },
570
+ product.id
571
+ );
572
+ }) })
573
+ ] });
574
+ }
575
+
576
+ // src/components/LumnGate.tsx
577
+ var import_react6 = require("react");
578
+
579
+ // src/hooks/useEntitlements.ts
580
+ var import_react5 = require("react");
581
+ function useEntitlements() {
582
+ const { lumn } = useLumnContext();
583
+ const checkEntitlement = (0, import_react5.useCallback)(
584
+ async (featureKey, customerId) => {
585
+ if (!customerId) {
586
+ return { entitled: false, feature_key: featureKey };
587
+ }
588
+ return lumn.entitlements.check({
589
+ customer_id: customerId,
590
+ feature_key: featureKey
591
+ });
592
+ },
593
+ [lumn]
594
+ );
595
+ const getUsageBalance = (0, import_react5.useCallback)(
596
+ async (featureKey, customerId) => {
597
+ const result = await checkEntitlement(featureKey, customerId);
598
+ return {
599
+ entitled: result.entitled,
600
+ feature_key: result.feature_key,
601
+ limit: result.limit,
602
+ unit_price_cents: result.unit_price_cents,
603
+ feature_type: result.feature_type,
604
+ used: result.used,
605
+ remaining: result.remaining,
606
+ reset_at: result.reset_at
607
+ };
608
+ },
609
+ [checkEntitlement]
610
+ );
611
+ return {
612
+ checkEntitlement,
613
+ getUsageBalance
614
+ };
615
+ }
616
+
617
+ // src/components/LumnGate.tsx
618
+ var import_jsx_runtime6 = require("react/jsx-runtime");
619
+ function LumnGate({
620
+ feature,
621
+ customerId,
622
+ fallback = null,
623
+ loadingFallback,
624
+ renderUsage,
625
+ children
626
+ }) {
627
+ const { checkEntitlement } = useEntitlements();
628
+ const [result, setResult] = (0, import_react6.useState)(null);
629
+ (0, import_react6.useEffect)(() => {
630
+ let cancelled = false;
631
+ setResult(null);
632
+ checkEntitlement(feature, customerId).then((res) => {
633
+ if (cancelled) return;
634
+ setResult(res);
635
+ }).catch(() => {
636
+ if (cancelled) return;
637
+ setResult({ entitled: false, feature_key: feature });
638
+ });
639
+ return () => {
640
+ cancelled = true;
641
+ };
642
+ }, [feature, customerId, checkEntitlement]);
643
+ if (result === null) {
644
+ return loadingFallback ?? null;
645
+ }
646
+ const entitled = result.entitled;
647
+ const hasRemaining = result.remaining === void 0 || result.remaining === null || result.remaining > 0;
648
+ const allowed = entitled && hasRemaining;
649
+ const usageData = result.used != null && result.remaining != null && result.reset_at != null ? { used: result.used, remaining: result.remaining, reset_at: result.reset_at } : null;
650
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
651
+ renderUsage && usageData ? renderUsage(usageData) : null,
652
+ allowed ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: fallback })
653
+ ] });
654
+ }
655
+
656
+ // src/hooks/useSubscriptions.ts
657
+ var import_react7 = require("react");
658
+ function useSubscriptions() {
659
+ const { lumn } = useLumnContext();
660
+ const getSubscriptionState = (0, import_react7.useCallback)(
661
+ async (customerId, productId) => {
662
+ const result = await lumn.subscriptions.list({
663
+ customer_id: customerId,
664
+ ...productId && { product_id: productId }
665
+ });
666
+ return result.data;
667
+ },
668
+ [lumn]
669
+ );
670
+ return {
671
+ getSubscriptionState
672
+ };
673
+ }
674
+
675
+ // src/hooks/useLumn.ts
676
+ function useLumn() {
677
+ const { checkEntitlement, getUsageBalance } = useEntitlements();
678
+ const { getSubscriptionState } = useSubscriptions();
679
+ return {
680
+ checkEntitlement,
681
+ getUsageBalance,
682
+ getSubscriptionState
683
+ };
684
+ }
685
+ // Annotate the CommonJS export names for ESM import in node:
686
+ 0 && (module.exports = {
687
+ LumnCheckout,
688
+ LumnGate,
689
+ LumnPricing,
690
+ LumnProvider,
691
+ formatIntervalPrice,
692
+ useEntitlements,
693
+ useLumn,
694
+ useLumnContext,
695
+ usePricing,
696
+ useSubscriptions
697
+ });