@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/README.md +72 -0
- package/dist/index.d.mts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +697 -0
- package/dist/index.mjs +661 -0
- package/package.json +65 -0
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
|
+
});
|