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