@nozle-js/react 0.3.0 → 0.5.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.cjs CHANGED
@@ -1,1009 +1,2885 @@
1
1
  'use strict';
2
2
 
3
- var react = require('react');
4
- var stripeJs = require('@stripe/stripe-js');
3
+ var React = require('react');
5
4
  var centrifuge = require('centrifuge');
6
5
  var jsxRuntime = require('react/jsx-runtime');
6
+ var reactStripeJs = require('@stripe/react-stripe-js');
7
+ var stripeJs = require('@stripe/stripe-js');
7
8
 
8
- // src/provider.tsx
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
10
 
10
- // src/store.ts
11
- var BillingStore = class {
12
- baseUrl;
13
- apiKey;
14
- customerId;
15
- state = {
16
- entitlements: null,
17
- credits: null,
18
- usage: {},
19
- connectionState: "connecting",
20
- error: null
11
+ var React__default = /*#__PURE__*/_interopDefault(React);
12
+
13
+ // src/provider.tsx
14
+ function createClient(apiKey, baseUrl) {
15
+ const base = baseUrl.replace(/\/+$/, "");
16
+ async function apiFetch(path, init) {
17
+ return fetch(`${base}${path}`, {
18
+ ...init,
19
+ headers: {
20
+ Authorization: `Bearer ${apiKey}`,
21
+ "Content-Type": "application/json",
22
+ ...init?.headers
23
+ }
24
+ });
25
+ }
26
+ return {
27
+ apiKey,
28
+ baseUrl: base,
29
+ fetch: apiFetch,
30
+ async can(customerId, feature) {
31
+ const res = await apiFetch(`/api/v1/can?customer_id=${encodeURIComponent(customerId)}&feature=${encodeURIComponent(feature)}`);
32
+ if (!res.ok) return { allowed: false, remaining: null, limit: null, used: 0 };
33
+ return res.json();
34
+ }
21
35
  };
22
- listeners = /* @__PURE__ */ new Set();
23
- constructor(baseUrl, apiKey, customerId) {
24
- this.baseUrl = baseUrl;
25
- this.apiKey = apiKey;
26
- this.customerId = customerId;
27
- }
28
- getSnapshot = () => this.state;
29
- getServerSnapshot = () => this.state;
30
- subscribe = (listener) => {
31
- this.listeners.add(listener);
36
+ }
37
+ var BillingContext = React.createContext({
38
+ client: null,
39
+ workspaceId: "",
40
+ centrifugoUrl: "",
41
+ centrifugoToken: null
42
+ });
43
+ function BillingProvider({
44
+ apiKey,
45
+ baseUrl = "https://api.nozle.app",
46
+ workspaceId = "",
47
+ centrifugoUrl,
48
+ children
49
+ }) {
50
+ const client = React.useMemo(() => createClient(apiKey, baseUrl), [apiKey, baseUrl]);
51
+ const [centrifugoToken, setCentrifugoToken] = React.useState(null);
52
+ const resolvedCentrifugoUrl = centrifugoUrl ?? (typeof process !== "undefined" ? process.env["NEXT_PUBLIC_CENTRIFUGO_URL"] ?? "" : "");
53
+ React.useEffect(() => {
54
+ let cancelled = false;
55
+ async function fetchCentrifugoToken() {
56
+ try {
57
+ const response = await client.fetch("/api/v1/auth/centrifugo-token");
58
+ if (!response.ok) return;
59
+ const data = await response.json();
60
+ if (!cancelled && data.token) {
61
+ setCentrifugoToken(data.token);
62
+ }
63
+ } catch {
64
+ }
65
+ }
66
+ void fetchCentrifugoToken();
32
67
  return () => {
33
- this.listeners.delete(listener);
68
+ cancelled = true;
34
69
  };
70
+ }, [client]);
71
+ const contextValue = {
72
+ client,
73
+ workspaceId,
74
+ centrifugoUrl: resolvedCentrifugoUrl,
75
+ centrifugoToken
35
76
  };
36
- emit() {
37
- for (const listener of this.listeners) {
38
- listener();
77
+ return React__default.default.createElement(BillingContext.Provider, { value: contextValue }, children);
78
+ }
79
+ function useBillingContext() {
80
+ return React.useContext(BillingContext);
81
+ }
82
+ function useNozleClient() {
83
+ const { client } = useBillingContext();
84
+ if (!client) throw new Error("useNozleClient must be used within <BillingProvider>");
85
+ return client;
86
+ }
87
+ var initialState = {
88
+ allowed: false,
89
+ remaining: null,
90
+ limit: null,
91
+ loading: true,
92
+ error: null
93
+ };
94
+ function useCan(customerId, featureKey) {
95
+ const { client, workspaceId, centrifugoUrl, centrifugoToken } = useBillingContext();
96
+ const [state, setState] = React.useState(initialState);
97
+ React.useEffect(() => {
98
+ if (!client) {
99
+ setState({
100
+ ...initialState,
101
+ loading: false,
102
+ error: "No client in context"
103
+ });
104
+ return;
39
105
  }
40
- }
41
- setConnectionState(state) {
42
- this.state = { ...this.state, connectionState: state };
43
- this.emit();
44
- }
45
- setError(error) {
46
- this.state = { ...this.state, error };
47
- this.emit();
48
- }
49
- updateUsage(metric, used, limit, remaining) {
50
- this.state = {
51
- ...this.state,
52
- usage: {
53
- ...this.state.usage,
54
- [metric]: { used, limit, remaining }
106
+ let cancelled = false;
107
+ setState(initialState);
108
+ async function checkEntitlement() {
109
+ try {
110
+ const result = await client.can(customerId, featureKey);
111
+ if (!cancelled) {
112
+ setState({
113
+ allowed: result.allowed,
114
+ remaining: result.remaining,
115
+ limit: result.limit,
116
+ loading: false,
117
+ error: result.error ?? null
118
+ });
119
+ }
120
+ } catch (err) {
121
+ if (!cancelled) {
122
+ const message = err instanceof Error ? err.message : "Unknown error";
123
+ setState({
124
+ allowed: false,
125
+ remaining: null,
126
+ limit: null,
127
+ loading: false,
128
+ error: message
129
+ });
130
+ }
55
131
  }
132
+ }
133
+ void checkEntitlement();
134
+ return () => {
135
+ cancelled = true;
56
136
  };
57
- this.emit();
58
- }
59
- async fetchUsage(feature) {
60
- try {
61
- const res = await fetch(
62
- `${this.baseUrl}/v1/can?customer_id=${this.customerId}&feature=${feature}`,
63
- { headers: { Authorization: `Bearer ${this.apiKey}` } }
64
- );
65
- if (!res.ok) return;
66
- const data = await res.json();
67
- this.updateUsage(feature, data.used, data.limit, data.remaining);
68
- this.state = {
69
- ...this.state,
70
- entitlements: {
71
- plan_slug: "pro",
72
- subscription_status: "active",
73
- features: { [feature]: { enabled: data.allowed } },
74
- limits: { [feature]: { limit: data.limit, used: data.used, source: "entitlement" } }
75
- }
76
- };
77
- this.emit();
78
- } catch (err) {
79
- this.state = {
80
- ...this.state,
81
- error: err instanceof Error ? err : new Error(String(err))
82
- };
83
- this.emit();
137
+ }, [client, customerId, featureKey]);
138
+ React.useEffect(() => {
139
+ if (!centrifugoToken || !centrifugoUrl || !workspaceId) {
140
+ return;
84
141
  }
85
- }
86
- async fetchCheckoutSecret(planCode, successUrl) {
87
- const res = await fetch(`${this.baseUrl}/v1/checkout`, {
88
- method: "POST",
89
- headers: {
90
- Authorization: `Bearer ${this.apiKey}`,
91
- "Content-Type": "application/json"
92
- },
93
- body: JSON.stringify({
94
- plan_code: planCode,
95
- customer_id: this.customerId,
96
- success_url: successUrl || `${window.location.origin}/checkout/complete?session_id={CHECKOUT_SESSION_ID}`
97
- })
142
+ const cf = new centrifuge.Centrifuge(centrifugoUrl, {
143
+ token: centrifugoToken
98
144
  });
99
- if (!res.ok) throw new Error("Failed to create checkout session");
100
- const data = await res.json();
101
- return data.client_secret;
102
- }
103
- async fetchPlans() {
104
- const res = await fetch(`${this.baseUrl}/v1/plans`, {
105
- headers: { Authorization: `Bearer ${this.apiKey}` }
145
+ const channel = `workspace:${workspaceId}:entitlements`;
146
+ const sub = cf.newSubscription(channel);
147
+ sub.on("publication", (ctx) => {
148
+ const data = ctx.data;
149
+ const pubCustomerId = data.customerId ?? data.customer_id;
150
+ const pubFeatureKey = data.featureKey ?? data.feature_key;
151
+ if (pubCustomerId === customerId && pubFeatureKey === featureKey) {
152
+ setState((prev) => ({
153
+ ...prev,
154
+ allowed: data.allowed ?? prev.allowed,
155
+ remaining: data.remaining !== void 0 ? data.remaining : prev.remaining,
156
+ limit: data.limit !== void 0 ? data.limit : prev.limit
157
+ }));
158
+ }
106
159
  });
107
- if (!res.ok) return [];
108
- const data = await res.json();
109
- return data.plans || [];
110
- }
111
- handleEvent(event) {
112
- if (event.type === "usage.updated") {
113
- const metric = event.metric;
114
- const used = event.used;
115
- const limit = event.limit;
116
- const remaining = event.remaining;
117
- this.updateUsage(metric, used, limit, remaining);
118
- }
119
- }
120
- };
121
- function connectCentrifugo(store, customerId, wsUrl) {
122
- const centrifuge$1 = new centrifuge.Centrifuge(wsUrl, {});
123
- const sub = centrifuge$1.newSubscription(
124
- `customer:${customerId}`
125
- );
126
- sub.on("publication", (ctx) => {
127
- store.handleEvent(
128
- ctx.data
129
- );
130
- });
131
- sub.on("subscribed", () => {
132
- store.setConnectionState("connected");
133
- });
134
- centrifuge$1.on("connected", () => {
135
- store.setConnectionState("connected");
136
- });
137
- centrifuge$1.on("disconnected", () => {
138
- store.setConnectionState("disconnected");
139
- });
140
- sub.subscribe();
141
- centrifuge$1.connect();
142
- store.setConnectionState("connecting");
143
- return {
144
- disconnect: () => {
160
+ sub.subscribe();
161
+ cf.connect();
162
+ return () => {
145
163
  sub.unsubscribe();
146
- centrifuge$1.disconnect();
164
+ cf.disconnect();
165
+ };
166
+ }, [centrifugoToken, centrifugoUrl, workspaceId, customerId, featureKey]);
167
+ return state;
168
+ }
169
+ function useUsage(customerId, metricKey) {
170
+ const { client } = useBillingContext();
171
+ const [state, setState] = React.useState({
172
+ value: null,
173
+ loading: true,
174
+ error: null
175
+ });
176
+ React.useEffect(() => {
177
+ if (!client) {
178
+ setState({ value: null, loading: false, error: "No client in context" });
179
+ return;
147
180
  }
148
- };
181
+ let cancelled = false;
182
+ async function fetchUsage() {
183
+ try {
184
+ const clientInternal = client;
185
+ const apiKey = clientInternal.apiKey ?? "";
186
+ const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
187
+ const timeout = clientInternal.timeout ?? 5e3;
188
+ const controller = new AbortController();
189
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
190
+ const response = await fetch(
191
+ `${baseUrl}/api/v1/usage/${customerId}/${metricKey}`,
192
+ {
193
+ method: "GET",
194
+ headers: {
195
+ Authorization: `Bearer ${apiKey}`
196
+ },
197
+ signal: controller.signal
198
+ }
199
+ );
200
+ clearTimeout(timeoutId);
201
+ if (!response.ok) {
202
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
203
+ }
204
+ const data = await response.json();
205
+ if (!cancelled) {
206
+ setState({
207
+ value: data.value ?? data.total ?? null,
208
+ loading: false,
209
+ error: null
210
+ });
211
+ }
212
+ } catch (err) {
213
+ if (!cancelled) {
214
+ const message = err instanceof Error ? err.message : "Unknown error";
215
+ setState({ value: null, loading: false, error: message });
216
+ }
217
+ }
218
+ }
219
+ void fetchUsage();
220
+ return () => {
221
+ cancelled = true;
222
+ };
223
+ }, [client, customerId, metricKey]);
224
+ return state;
149
225
  }
150
- var BillingContext = react.createContext(null);
151
- function BillingProvider({
152
- apiKey,
153
- customerId,
154
- baseUrl = "http://localhost:8080",
155
- wsUrl = "ws://localhost:8001/connection/websocket",
156
- stripeKey,
157
- features = [],
158
- children
159
- }) {
160
- const storeRef = react.useRef(null);
161
- if (!storeRef.current) {
162
- storeRef.current = new BillingStore(baseUrl, apiKey, customerId);
163
- }
164
- const stripePromise = react.useMemo(
165
- () => stripeKey ? stripeJs.loadStripe(stripeKey) : null,
166
- [stripeKey]
167
- );
168
- react.useEffect(() => {
169
- const store = storeRef.current;
170
- for (const feature of features) {
171
- store.fetchUsage(feature);
172
- }
173
- const connection = connectCentrifugo(
174
- store,
175
- customerId,
176
- wsUrl
177
- );
226
+ function useCredits(customerId) {
227
+ const { client } = useBillingContext();
228
+ const [state, setState] = React.useState({
229
+ balance: null,
230
+ loading: true,
231
+ error: null
232
+ });
233
+ React.useEffect(() => {
234
+ if (!client) {
235
+ setState({
236
+ balance: null,
237
+ loading: false,
238
+ error: "No client in context"
239
+ });
240
+ return;
241
+ }
242
+ let cancelled = false;
243
+ async function fetchCredits() {
244
+ try {
245
+ const clientInternal = client;
246
+ const apiKey = clientInternal.apiKey ?? "";
247
+ const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
248
+ const timeout = clientInternal.timeout ?? 5e3;
249
+ const controller = new AbortController();
250
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
251
+ const response = await fetch(
252
+ `${baseUrl}/api/v1/credits/${customerId}/balance`,
253
+ {
254
+ method: "GET",
255
+ headers: {
256
+ Authorization: `Bearer ${apiKey}`
257
+ },
258
+ signal: controller.signal
259
+ }
260
+ );
261
+ clearTimeout(timeoutId);
262
+ if (!response.ok) {
263
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
264
+ }
265
+ const data = await response.json();
266
+ if (!cancelled) {
267
+ setState({
268
+ balance: data.balance ?? data.amount ?? null,
269
+ loading: false,
270
+ error: null
271
+ });
272
+ }
273
+ } catch (err) {
274
+ if (!cancelled) {
275
+ const message = err instanceof Error ? err.message : "Unknown error";
276
+ setState({ balance: null, loading: false, error: message });
277
+ }
278
+ }
279
+ }
280
+ void fetchCredits();
178
281
  return () => {
179
- connection.disconnect();
282
+ cancelled = true;
180
283
  };
181
- }, [apiKey, customerId, baseUrl, wsUrl, features]);
182
- return /* @__PURE__ */ jsxRuntime.jsx(BillingContext.Provider, { value: { store: storeRef.current, stripePromise }, children });
284
+ }, [client, customerId]);
285
+ return state;
183
286
  }
184
- function useBillingContext() {
185
- const ctx = react.useContext(BillingContext);
287
+ var BillingPortalContext = React.createContext(
288
+ null
289
+ );
290
+ function useBillingPortal() {
291
+ const ctx = React.useContext(BillingPortalContext);
186
292
  if (!ctx) {
187
- throw new Error("useBillingContext must be used within a BillingProvider");
293
+ throw new Error(
294
+ "useBillingPortal must be used inside BillingPortalProvider"
295
+ );
188
296
  }
189
297
  return ctx;
190
298
  }
191
- function useCan(feature) {
192
- const { store } = useBillingContext();
193
- const state = react.useSyncExternalStore(
194
- store.subscribe,
195
- store.getSnapshot,
196
- store.getServerSnapshot
299
+ function BillingPortalProvider({
300
+ customerId,
301
+ apiKey,
302
+ children
303
+ }) {
304
+ return /* @__PURE__ */ jsxRuntime.jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
305
+ }
306
+ function PlanCard({
307
+ id,
308
+ name,
309
+ monthlyPrice,
310
+ annualPrice,
311
+ features,
312
+ description,
313
+ isAnnual,
314
+ isCurrent,
315
+ onSelect
316
+ }) {
317
+ const price = isAnnual ? annualPrice : monthlyPrice;
318
+ const interval = isAnnual ? "year" : "month";
319
+ return /* @__PURE__ */ jsxRuntime.jsxs(
320
+ "div",
321
+ {
322
+ "data-testid": `plan-card-${id}`,
323
+ "aria-current": isCurrent ? "true" : void 0,
324
+ style: {
325
+ background: "var(--nozle-background, var(--background))",
326
+ border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
327
+ borderRadius: "var(--nozle-radius, 0.5rem)",
328
+ padding: "1.5rem",
329
+ display: "flex",
330
+ flexDirection: "column",
331
+ gap: "1rem"
332
+ },
333
+ className: isCurrent ? "border-primary" : "",
334
+ children: [
335
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
336
+ /* @__PURE__ */ jsxRuntime.jsx(
337
+ "h3",
338
+ {
339
+ style: {
340
+ fontSize: "1.25rem",
341
+ fontWeight: 600,
342
+ color: "var(--nozle-foreground, var(--foreground))",
343
+ margin: 0
344
+ },
345
+ children: name
346
+ }
347
+ ),
348
+ description && /* @__PURE__ */ jsxRuntime.jsx(
349
+ "p",
350
+ {
351
+ style: {
352
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
353
+ fontSize: "0.875rem",
354
+ margin: "0.25rem 0 0"
355
+ },
356
+ children: description
357
+ }
358
+ )
359
+ ] }),
360
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
361
+ /* @__PURE__ */ jsxRuntime.jsxs(
362
+ "span",
363
+ {
364
+ style: {
365
+ fontSize: "2rem",
366
+ fontWeight: 700,
367
+ color: "var(--nozle-foreground, var(--foreground))"
368
+ },
369
+ children: [
370
+ "$",
371
+ price
372
+ ]
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsxRuntime.jsxs(
376
+ "span",
377
+ {
378
+ style: {
379
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
380
+ marginLeft: "0.25rem"
381
+ },
382
+ children: [
383
+ "/",
384
+ interval
385
+ ]
386
+ }
387
+ )
388
+ ] }),
389
+ /* @__PURE__ */ jsxRuntime.jsx(
390
+ "ul",
391
+ {
392
+ style: {
393
+ listStyle: "none",
394
+ padding: 0,
395
+ margin: 0,
396
+ display: "flex",
397
+ flexDirection: "column",
398
+ gap: "0.5rem",
399
+ flex: 1
400
+ },
401
+ children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
402
+ "li",
403
+ {
404
+ style: {
405
+ display: "flex",
406
+ alignItems: "center",
407
+ gap: "0.5rem",
408
+ color: "var(--nozle-foreground, var(--foreground))",
409
+ fontSize: "0.875rem"
410
+ },
411
+ children: [
412
+ /* @__PURE__ */ jsxRuntime.jsx(
413
+ "span",
414
+ {
415
+ style: { color: "var(--nozle-primary, var(--primary))" },
416
+ "aria-hidden": "true",
417
+ children: "\u2713"
418
+ }
419
+ ),
420
+ feature
421
+ ]
422
+ },
423
+ feature
424
+ ))
425
+ }
426
+ ),
427
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
428
+ "button",
429
+ {
430
+ onClick: onSelect,
431
+ style: {
432
+ background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
433
+ color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
434
+ border: "none",
435
+ borderRadius: "var(--nozle-radius, 0.5rem)",
436
+ padding: "0.75rem 1.5rem",
437
+ cursor: isCurrent ? "default" : "pointer",
438
+ fontWeight: 500,
439
+ width: "100%"
440
+ },
441
+ disabled: isCurrent,
442
+ "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
443
+ children: isCurrent ? "Current Plan" : "Get Started"
444
+ }
445
+ )
446
+ ]
447
+ }
197
448
  );
198
- if (state.error) {
199
- return { allowed: false, isLoading: false, error: state.error };
200
- }
201
- if (state.entitlements === null) {
202
- return { allowed: false, isLoading: true, error: null };
203
- }
204
- const featureState = state.entitlements.features[feature];
205
- return {
206
- allowed: featureState?.enabled ?? false,
207
- isLoading: false,
208
- error: null
209
- };
210
449
  }
211
- function useUsage(metric) {
212
- const { store } = useBillingContext();
213
- const state = react.useSyncExternalStore(
214
- store.subscribe,
215
- store.getSnapshot,
216
- store.getServerSnapshot
450
+ function PricingTable({
451
+ plans,
452
+ currentPlanId,
453
+ onSelect
454
+ }) {
455
+ const [isAnnual, setIsAnnual] = React.useState(false);
456
+ return /* @__PURE__ */ jsxRuntime.jsxs(
457
+ "div",
458
+ {
459
+ style: {
460
+ background: "var(--nozle-background, var(--background))",
461
+ color: "var(--nozle-foreground, var(--foreground))"
462
+ },
463
+ children: [
464
+ /* @__PURE__ */ jsxRuntime.jsxs(
465
+ "div",
466
+ {
467
+ style: {
468
+ display: "flex",
469
+ gap: "0.5rem",
470
+ justifyContent: "center",
471
+ marginBottom: "1.5rem"
472
+ },
473
+ children: [
474
+ /* @__PURE__ */ jsxRuntime.jsx(
475
+ "button",
476
+ {
477
+ onClick: () => setIsAnnual(false),
478
+ style: {
479
+ padding: "0.5rem 1rem",
480
+ borderRadius: "var(--nozle-radius, 0.5rem)",
481
+ border: "1px solid var(--nozle-border, var(--border))",
482
+ background: !isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
483
+ color: !isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
484
+ cursor: "pointer",
485
+ fontWeight: !isAnnual ? 600 : 400
486
+ },
487
+ "aria-pressed": !isAnnual,
488
+ children: "Monthly"
489
+ }
490
+ ),
491
+ /* @__PURE__ */ jsxRuntime.jsx(
492
+ "button",
493
+ {
494
+ onClick: () => setIsAnnual(true),
495
+ style: {
496
+ padding: "0.5rem 1rem",
497
+ borderRadius: "var(--nozle-radius, 0.5rem)",
498
+ border: "1px solid var(--nozle-border, var(--border))",
499
+ background: isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
500
+ color: isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
501
+ cursor: "pointer",
502
+ fontWeight: isAnnual ? 600 : 400
503
+ },
504
+ "aria-pressed": isAnnual,
505
+ children: "Annual"
506
+ }
507
+ )
508
+ ]
509
+ }
510
+ ),
511
+ /* @__PURE__ */ jsxRuntime.jsx(
512
+ "div",
513
+ {
514
+ style: {
515
+ display: "grid",
516
+ gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
517
+ gap: "1rem"
518
+ },
519
+ children: plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
520
+ PlanCard,
521
+ {
522
+ ...plan,
523
+ isAnnual,
524
+ isCurrent: plan.id === currentPlanId,
525
+ onSelect: onSelect ? () => onSelect(plan.id) : void 0
526
+ },
527
+ plan.id
528
+ ))
529
+ }
530
+ )
531
+ ]
532
+ }
217
533
  );
218
- if (state.error) {
219
- return { data: null, isLoading: false, error: state.error };
220
- }
221
- if (state.entitlements === null) {
222
- return { data: null, isLoading: true, error: null };
223
- }
224
- const usage = state.usage[metric] ?? null;
225
- return { data: usage, isLoading: false, error: null };
226
534
  }
227
- function usePlan() {
228
- const { store } = useBillingContext();
229
- const state = react.useSyncExternalStore(
230
- store.subscribe,
231
- store.getSnapshot,
232
- store.getServerSnapshot
535
+ function PlanComparison({
536
+ plans,
537
+ features
538
+ }) {
539
+ return /* @__PURE__ */ jsxRuntime.jsx(
540
+ "div",
541
+ {
542
+ style: {
543
+ overflowX: "auto",
544
+ background: "var(--nozle-background, var(--background))"
545
+ },
546
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
547
+ "table",
548
+ {
549
+ style: {
550
+ width: "100%",
551
+ borderCollapse: "collapse",
552
+ fontSize: "0.875rem",
553
+ color: "var(--nozle-foreground, var(--foreground))"
554
+ },
555
+ children: [
556
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
557
+ /* @__PURE__ */ jsxRuntime.jsx(
558
+ "th",
559
+ {
560
+ style: {
561
+ padding: "0.75rem 1rem",
562
+ textAlign: "left",
563
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
564
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
565
+ fontWeight: 500
566
+ },
567
+ children: "Feature"
568
+ }
569
+ ),
570
+ plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
571
+ "th",
572
+ {
573
+ style: {
574
+ padding: "0.75rem 1rem",
575
+ textAlign: "center",
576
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
577
+ fontWeight: 600,
578
+ color: "var(--nozle-foreground, var(--foreground))"
579
+ },
580
+ children: plan.name
581
+ },
582
+ plan.id
583
+ ))
584
+ ] }) }),
585
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
586
+ /* @__PURE__ */ jsxRuntime.jsx(
587
+ "td",
588
+ {
589
+ style: {
590
+ padding: "0.75rem 1rem",
591
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
592
+ color: "var(--nozle-foreground, var(--foreground))"
593
+ },
594
+ children: feature.label
595
+ }
596
+ ),
597
+ plans.map((plan) => {
598
+ const value = feature.values[plan.id];
599
+ return /* @__PURE__ */ jsxRuntime.jsx(
600
+ "td",
601
+ {
602
+ style: {
603
+ padding: "0.75rem 1rem",
604
+ textAlign: "center",
605
+ borderBottom: "1px solid var(--nozle-border, var(--border))"
606
+ },
607
+ children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsxRuntime.jsx(
608
+ "span",
609
+ {
610
+ style: {
611
+ color: "var(--nozle-primary, var(--primary))"
612
+ },
613
+ "aria-label": "Included",
614
+ children: "\u2713"
615
+ }
616
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
617
+ "span",
618
+ {
619
+ style: {
620
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
621
+ },
622
+ "aria-label": "Not included",
623
+ children: "\u2715"
624
+ }
625
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: value ?? "\u2014" })
626
+ },
627
+ plan.id
628
+ );
629
+ })
630
+ ] }, feature.key)) })
631
+ ]
632
+ }
633
+ )
634
+ }
233
635
  );
234
- if (state.error) {
235
- return { data: null, isLoading: false, error: state.error };
236
- }
237
- if (state.entitlements === null) {
238
- return { data: null, isLoading: true, error: null };
239
- }
240
- return {
241
- data: {
242
- plan_slug: state.entitlements.plan_slug,
243
- subscription_status: state.entitlements.subscription_status
244
- },
245
- isLoading: false,
246
- error: null
247
- };
248
636
  }
249
- function useCheckout() {
250
- const { store, stripePromise } = useBillingContext();
251
- const [isLoading, setIsLoading] = react.useState(false);
252
- const [error, setError] = react.useState(null);
253
- const fetchClientSecret = react.useCallback(
254
- async (planCode, successUrl) => {
255
- setIsLoading(true);
256
- setError(null);
257
- try {
258
- return await store.fetchCheckoutSecret(planCode, successUrl);
259
- } catch (err) {
260
- const e = err instanceof Error ? err : new Error(String(err));
261
- setError(e);
262
- throw e;
263
- } finally {
264
- setIsLoading(false);
637
+ function loadRazorpayScript() {
638
+ return new Promise((resolve, reject) => {
639
+ if (document.getElementById("razorpay-js")) {
640
+ resolve();
641
+ return;
642
+ }
643
+ const script = document.createElement("script");
644
+ script.id = "razorpay-js";
645
+ script.src = "https://checkout.razorpay.com/v1/checkout.js";
646
+ script.onload = () => resolve();
647
+ script.onerror = () => reject(new Error("Failed to load Razorpay.js"));
648
+ document.body.appendChild(script);
649
+ });
650
+ }
651
+ function CheckoutButton({
652
+ planId,
653
+ label = "Get Started",
654
+ apiBaseUrl = "https://api.nozle.app",
655
+ className,
656
+ style,
657
+ onError,
658
+ razorpayKeyId,
659
+ onStripeClientSecret,
660
+ onSuccess
661
+ }) {
662
+ const { customerId, apiKey } = useBillingPortal();
663
+ const [loading, setLoading] = React.useState(false);
664
+ async function handleClick() {
665
+ setLoading(true);
666
+ try {
667
+ const response = await fetch(`${apiBaseUrl}/api/v1/checkout`, {
668
+ method: "POST",
669
+ headers: {
670
+ "Content-Type": "application/json",
671
+ Authorization: `Bearer ${apiKey}`
672
+ },
673
+ body: JSON.stringify({ planId, customerId })
674
+ });
675
+ if (!response.ok) {
676
+ throw new Error(`HTTP ${response.status}: Checkout failed`);
265
677
  }
266
- },
267
- [store]
268
- );
269
- return { fetchClientSecret, stripePromise, isLoading, error };
270
- }
271
- function useSubscribe() {
272
- const { store } = useBillingContext();
273
- const [isLoading, setIsLoading] = react.useState(false);
274
- const [error, setError] = react.useState(null);
275
- const subscribe = react.useCallback(
276
- async (planCode) => {
277
- setIsLoading(true);
278
- setError(null);
279
- try {
280
- const res = await fetch(`${store.baseUrl}/v1/subscribe`, {
281
- method: "POST",
282
- headers: {
283
- Authorization: `Bearer ${store.apiKey}`,
284
- "Content-Type": "application/json"
678
+ const data = await response.json();
679
+ if ("type" in data && data.type === "razorpay") {
680
+ if (!razorpayKeyId) {
681
+ onError?.(
682
+ new Error("razorpayKeyId prop required for Razorpay checkout")
683
+ );
684
+ return;
685
+ }
686
+ await loadRazorpayScript();
687
+ const rzp = new window.Razorpay({
688
+ key: razorpayKeyId,
689
+ order_id: data.orderId,
690
+ handler: (r) => {
691
+ onSuccess?.(r.razorpay_payment_id);
285
692
  },
286
- body: JSON.stringify({
287
- plan_code: planCode,
288
- customer_id: store.customerId
289
- })
693
+ modal: {
694
+ ondismiss: () => setLoading(false)
695
+ }
290
696
  });
291
- if (!res.ok) throw new Error("Failed to create subscription");
292
- return await res.json();
293
- } catch (err) {
294
- const e = err instanceof Error ? err : new Error(String(err));
295
- setError(e);
296
- throw e;
297
- } finally {
298
- setIsLoading(false);
697
+ rzp.open();
698
+ } else if ("type" in data && data.type === "stripe") {
699
+ if (data.url) {
700
+ window.location.href = data.url;
701
+ } else if (data.clientSecret) {
702
+ onStripeClientSecret?.(data.clientSecret);
703
+ } else {
704
+ onError?.(
705
+ new Error("Stripe checkout: no url or clientSecret in response")
706
+ );
707
+ }
708
+ } else if ("url" in data && data.url) {
709
+ window.location.href = data.url;
710
+ } else {
711
+ onError?.(new Error("Unknown checkout response format"));
299
712
  }
300
- },
301
- [store]
713
+ } catch (err) {
714
+ const error = err instanceof Error ? err : new Error("Checkout failed");
715
+ onError?.(error);
716
+ } finally {
717
+ setLoading(false);
718
+ }
719
+ }
720
+ return /* @__PURE__ */ jsxRuntime.jsx(
721
+ "button",
722
+ {
723
+ onClick: () => void handleClick(),
724
+ disabled: loading,
725
+ className,
726
+ style: {
727
+ padding: "0.75rem 1.5rem",
728
+ borderRadius: "var(--nozle-radius, 0.5rem)",
729
+ border: "none",
730
+ background: "var(--nozle-primary, var(--primary))",
731
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
732
+ cursor: loading ? "not-allowed" : "pointer",
733
+ fontWeight: 500,
734
+ opacity: loading ? 0.7 : 1,
735
+ ...style
736
+ },
737
+ "aria-busy": loading,
738
+ children: loading ? "Loading..." : label
739
+ }
302
740
  );
303
- return { subscribe, isLoading, error };
304
- }
305
- function usePlans() {
306
- const { store } = useBillingContext();
307
- const [plans, setPlans] = react.useState([]);
308
- const [isLoading, setIsLoading] = react.useState(true);
309
- react.useEffect(() => {
310
- store.fetchPlans().then(setPlans).finally(() => setIsLoading(false));
311
- }, [store]);
312
- return { plans, isLoading };
313
- }
314
- var fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
315
- var levelColors = {
316
- normal: {
317
- fg: "#6366f1",
318
- bg: "rgba(99, 102, 241, 0.08)",
319
- trackBg: "#e5e7eb"
320
- },
321
- warning: {
322
- fg: "#f59e0b",
323
- bg: "rgba(245, 158, 11, 0.08)",
324
- trackBg: "#e5e7eb"
325
- },
326
- critical: {
327
- fg: "#ef4444",
328
- bg: "rgba(239, 68, 68, 0.08)",
329
- trackBg: "#e5e7eb"
741
+ }
742
+ var CheckoutContext = React.createContext(null);
743
+ function useCheckout() {
744
+ const ctx = React.useContext(CheckoutContext);
745
+ if (ctx === null) {
746
+ throw new Error("useCheckout must be used inside a <Checkout> component");
330
747
  }
331
- };
332
- var skeletonStyleInjected = false;
333
- function ensureKeyframes() {
334
- if (skeletonStyleInjected || typeof document === "undefined") return;
335
- const sheet = document.createElement("style");
336
- sheet.textContent = `@keyframes nozle-pulse{0%,100%{opacity:1}50%{opacity:.4}}`;
337
- document.head.appendChild(sheet);
338
- skeletonStyleInjected = true;
748
+ return ctx;
339
749
  }
340
- function UsageMeter({
341
- metric,
342
- variant = "bar",
750
+ function buildStripeAppearance() {
751
+ if (typeof document === "undefined") {
752
+ return { theme: "stripe" };
753
+ }
754
+ const style = getComputedStyle(document.documentElement);
755
+ const get = (prop, fallback) => style.getPropertyValue(prop).trim() || fallback;
756
+ return {
757
+ theme: "stripe",
758
+ variables: {
759
+ colorPrimary: get("--nozle-primary", "#16a34a"),
760
+ colorBackground: get("--nozle-background", "#f8fafc"),
761
+ colorText: get("--nozle-foreground", "#1e293b"),
762
+ colorDanger: get("--nozle-destructive", "#dc2626"),
763
+ borderRadius: get("--nozle-radius", "0.5rem"),
764
+ fontFamily: "inherit"
765
+ }
766
+ };
767
+ }
768
+ function CheckoutInner({
769
+ submitLabel,
770
+ onSuccess,
771
+ onError,
772
+ onReady,
343
773
  className,
344
- style,
345
- ...props
774
+ style
346
775
  }) {
347
- const { data, isLoading, error } = useUsage(metric);
348
- if (isLoading) {
349
- ensureKeyframes();
350
- if (variant === "minimal") {
351
- return /* @__PURE__ */ jsxRuntime.jsx(
352
- "span",
353
- {
354
- "data-nozle": "usage-meter",
355
- "data-variant": "minimal",
356
- className,
357
- style: {
358
- display: "inline-block",
359
- width: 80,
360
- height: 16,
361
- borderRadius: 4,
362
- background: "#e5e7eb",
363
- animation: "nozle-pulse 1.5s ease-in-out infinite",
364
- ...style
365
- },
366
- ...props
776
+ const stripe = reactStripeJs.useStripe();
777
+ const elements = reactStripeJs.useElements();
778
+ const [isProcessing, setIsProcessing] = React.useState(false);
779
+ const [error, setError] = React.useState(null);
780
+ const [isReady, setIsReady] = React.useState(false);
781
+ async function confirmPayment() {
782
+ if (!stripe || !elements) return;
783
+ setIsProcessing(true);
784
+ setError(null);
785
+ const result = await stripe.confirmPayment({
786
+ elements,
787
+ confirmParams: { return_url: window.location.href },
788
+ redirect: "if_required"
789
+ });
790
+ setIsProcessing(false);
791
+ if (result.error) {
792
+ const err = new Error(result.error.message ?? "Payment failed");
793
+ setError(err);
794
+ onError?.(err);
795
+ } else if (result.paymentIntent?.status === "succeeded") {
796
+ onSuccess?.(result.paymentIntent.id);
797
+ }
798
+ }
799
+ return /* @__PURE__ */ jsxRuntime.jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style, children: [
800
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes nozle-skeleton-pulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }` }),
801
+ !isReady && /* @__PURE__ */ jsxRuntime.jsx(
802
+ "div",
803
+ {
804
+ "data-testid": "checkout-skeleton",
805
+ style: {
806
+ height: "200px",
807
+ borderRadius: "var(--nozle-radius, 0.5rem)",
808
+ background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
809
+ backgroundSize: "200% 100%",
810
+ animation: "nozle-skeleton-pulse 1.5s ease-in-out infinite"
367
811
  }
368
- );
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsxRuntime.jsx(
815
+ reactStripeJs.PaymentElement,
816
+ {
817
+ onReady: () => {
818
+ setIsReady(true);
819
+ onReady?.();
820
+ }
821
+ }
822
+ ),
823
+ error !== null && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "var(--nozle-destructive, #dc2626)", margin: "0.5rem 0 0" }, children: error.message }),
824
+ /* @__PURE__ */ jsxRuntime.jsx(
825
+ "button",
826
+ {
827
+ type: "button",
828
+ onClick: () => void confirmPayment(),
829
+ disabled: isProcessing || !stripe,
830
+ "aria-busy": isProcessing,
831
+ style: {
832
+ padding: "0.75rem 1.5rem",
833
+ borderRadius: "var(--nozle-radius, 0.5rem)",
834
+ border: "none",
835
+ background: "var(--nozle-primary, var(--primary))",
836
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
837
+ cursor: isProcessing ? "not-allowed" : "pointer",
838
+ fontWeight: 500,
839
+ opacity: isProcessing ? 0.7 : 1,
840
+ marginTop: "1rem",
841
+ width: "100%"
842
+ },
843
+ children: isProcessing ? "Processing..." : submitLabel ?? "Pay now"
844
+ }
845
+ )
846
+ ] }) });
847
+ }
848
+ function Checkout({
849
+ clientSecret,
850
+ publishableKey,
851
+ stripeAccount,
852
+ submitLabel,
853
+ onSuccess,
854
+ onError,
855
+ onReady,
856
+ className,
857
+ style
858
+ }) {
859
+ const stripePromise = React.useMemo(
860
+ () => stripeJs.loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
861
+ [publishableKey, stripeAccount]
862
+ );
863
+ const appearance = React.useMemo(() => buildStripeAppearance(), []);
864
+ const options = { clientSecret, appearance };
865
+ return /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsxRuntime.jsx(
866
+ CheckoutInner,
867
+ {
868
+ submitLabel,
869
+ onSuccess,
870
+ onError,
871
+ onReady,
872
+ className,
873
+ style
874
+ }
875
+ ) }, clientSecret);
876
+ }
877
+ function UpgradeModal({
878
+ isOpen,
879
+ targetPlanId,
880
+ customerId,
881
+ apiBaseUrl = "https://api.nozle.app",
882
+ apiKey = "",
883
+ onConfirm,
884
+ onCancel
885
+ }) {
886
+ const [preview, setPreview] = React.useState(null);
887
+ const [loading, setLoading] = React.useState(false);
888
+ const [error, setError] = React.useState(null);
889
+ const [confirming, setConfirming] = React.useState(false);
890
+ React.useEffect(() => {
891
+ if (!isOpen || !targetPlanId || !customerId) {
892
+ return;
893
+ }
894
+ let cancelled = false;
895
+ setLoading(true);
896
+ setError(null);
897
+ setPreview(null);
898
+ async function fetchPreview() {
899
+ try {
900
+ const response = await fetch(
901
+ `${apiBaseUrl}/api/v1/subscriptions/preview`,
902
+ {
903
+ method: "POST",
904
+ headers: {
905
+ "Content-Type": "application/json",
906
+ Authorization: `Bearer ${apiKey}`
907
+ },
908
+ body: JSON.stringify({ targetPlanId, customerId })
909
+ }
910
+ );
911
+ if (!response.ok) {
912
+ throw new Error(
913
+ `HTTP ${response.status}: Failed to load proration preview`
914
+ );
915
+ }
916
+ const data = await response.json();
917
+ if (!cancelled) {
918
+ setPreview(data);
919
+ }
920
+ } catch (err) {
921
+ if (!cancelled) {
922
+ const message = err instanceof Error ? err.message : "Failed to load preview";
923
+ setError(message);
924
+ }
925
+ } finally {
926
+ if (!cancelled) {
927
+ setLoading(false);
928
+ }
929
+ }
930
+ }
931
+ void fetchPreview();
932
+ return () => {
933
+ cancelled = true;
934
+ };
935
+ }, [isOpen, targetPlanId, customerId, apiBaseUrl, apiKey]);
936
+ async function handleConfirm() {
937
+ setConfirming(true);
938
+ try {
939
+ const response = await fetch(
940
+ `${apiBaseUrl}/api/v1/subscriptions/change`,
941
+ {
942
+ method: "POST",
943
+ headers: {
944
+ "Content-Type": "application/json",
945
+ Authorization: `Bearer ${apiKey}`
946
+ },
947
+ body: JSON.stringify({ targetPlanId, customerId })
948
+ }
949
+ );
950
+ if (!response.ok) {
951
+ throw new Error(`HTTP ${response.status}: Upgrade failed`);
952
+ }
953
+ onConfirm?.();
954
+ } catch (err) {
955
+ const message = err instanceof Error ? err.message : "Upgrade failed";
956
+ setError(message);
957
+ } finally {
958
+ setConfirming(false);
959
+ }
960
+ }
961
+ if (!isOpen) {
962
+ return null;
963
+ }
964
+ const overlayStyle = {
965
+ position: "fixed",
966
+ inset: 0,
967
+ background: "oklch(0 0 0 / 0.5)",
968
+ display: "flex",
969
+ alignItems: "center",
970
+ justifyContent: "center",
971
+ zIndex: 1e3
972
+ };
973
+ const modalStyle = {
974
+ background: "var(--nozle-background, var(--background))",
975
+ color: "var(--nozle-foreground, var(--foreground))",
976
+ border: "1px solid var(--nozle-border, var(--border))",
977
+ borderRadius: "var(--nozle-radius, 0.5rem)",
978
+ padding: "2rem",
979
+ maxWidth: "28rem",
980
+ width: "100%",
981
+ boxShadow: "var(--nozle-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1))"
982
+ };
983
+ return /* @__PURE__ */ jsxRuntime.jsx(
984
+ "div",
985
+ {
986
+ style: overlayStyle,
987
+ role: "dialog",
988
+ "aria-modal": "true",
989
+ "aria-label": "Upgrade plan",
990
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: modalStyle, children: [
991
+ /* @__PURE__ */ jsxRuntime.jsx(
992
+ "h2",
993
+ {
994
+ style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" },
995
+ children: "Confirm Plan Upgrade"
996
+ }
997
+ ),
998
+ loading && /* @__PURE__ */ jsxRuntime.jsx(
999
+ "p",
1000
+ {
1001
+ style: {
1002
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1003
+ },
1004
+ children: "Loading proration preview..."
1005
+ }
1006
+ ),
1007
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "oklch(0.6 0.2 25)", marginBottom: "1rem" }, children: error }),
1008
+ preview && !loading && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1009
+ /* @__PURE__ */ jsxRuntime.jsxs(
1010
+ "div",
1011
+ {
1012
+ style: {
1013
+ background: "var(--nozle-muted, var(--muted))",
1014
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1015
+ padding: "1rem",
1016
+ display: "flex",
1017
+ flexDirection: "column",
1018
+ gap: "0.5rem"
1019
+ },
1020
+ children: [
1021
+ preview.credit > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1022
+ "div",
1023
+ {
1024
+ style: { display: "flex", justifyContent: "space-between" },
1025
+ children: [
1026
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Credit from current plan" }),
1027
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "oklch(0.6 0.15 150)" }, children: [
1028
+ "-$",
1029
+ preview.credit.toFixed(2)
1030
+ ] })
1031
+ ]
1032
+ }
1033
+ ),
1034
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
1035
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "New plan charge" }),
1036
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1037
+ "$",
1038
+ preview.debit.toFixed(2)
1039
+ ] })
1040
+ ] }),
1041
+ /* @__PURE__ */ jsxRuntime.jsxs(
1042
+ "div",
1043
+ {
1044
+ style: {
1045
+ display: "flex",
1046
+ justifyContent: "space-between",
1047
+ fontWeight: 600,
1048
+ borderTop: "1px solid var(--nozle-border, var(--border))",
1049
+ paddingTop: "0.5rem",
1050
+ marginTop: "0.25rem"
1051
+ },
1052
+ children: [
1053
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "You'll be charged today" }),
1054
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1055
+ "$",
1056
+ preview.net.toFixed(2)
1057
+ ] })
1058
+ ]
1059
+ }
1060
+ )
1061
+ ]
1062
+ }
1063
+ ),
1064
+ /* @__PURE__ */ jsxRuntime.jsxs(
1065
+ "p",
1066
+ {
1067
+ style: {
1068
+ fontSize: "0.875rem",
1069
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
1070
+ marginTop: "0.75rem"
1071
+ },
1072
+ children: [
1073
+ "Your next billing date is",
1074
+ " ",
1075
+ new Date(preview.nextBillingDate).toLocaleDateString(),
1076
+ "."
1077
+ ]
1078
+ }
1079
+ )
1080
+ ] }),
1081
+ /* @__PURE__ */ jsxRuntime.jsxs(
1082
+ "div",
1083
+ {
1084
+ style: {
1085
+ display: "flex",
1086
+ gap: "0.75rem",
1087
+ justifyContent: "flex-end"
1088
+ },
1089
+ children: [
1090
+ /* @__PURE__ */ jsxRuntime.jsx(
1091
+ "button",
1092
+ {
1093
+ onClick: onCancel,
1094
+ style: {
1095
+ padding: "0.5rem 1rem",
1096
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1097
+ border: "1px solid var(--nozle-border, var(--border))",
1098
+ background: "transparent",
1099
+ color: "var(--nozle-foreground, var(--foreground))",
1100
+ cursor: "pointer"
1101
+ },
1102
+ disabled: confirming,
1103
+ children: "Cancel"
1104
+ }
1105
+ ),
1106
+ /* @__PURE__ */ jsxRuntime.jsx(
1107
+ "button",
1108
+ {
1109
+ onClick: () => void handleConfirm(),
1110
+ style: {
1111
+ padding: "0.5rem 1rem",
1112
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1113
+ border: "none",
1114
+ background: "var(--nozle-primary, var(--primary))",
1115
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1116
+ cursor: confirming || loading ? "not-allowed" : "pointer",
1117
+ fontWeight: 500,
1118
+ opacity: confirming || loading ? 0.7 : 1
1119
+ },
1120
+ disabled: confirming || loading || !!error,
1121
+ children: confirming ? "Upgrading..." : "Confirm Upgrade"
1122
+ }
1123
+ )
1124
+ ]
1125
+ }
1126
+ )
1127
+ ] })
1128
+ }
1129
+ );
1130
+ }
1131
+ function UpgradeButton({
1132
+ targetPlanId,
1133
+ label = "Upgrade",
1134
+ apiBaseUrl = "https://api.nozle.app",
1135
+ className,
1136
+ style,
1137
+ onUpgraded
1138
+ }) {
1139
+ const { customerId, apiKey } = useBillingPortal();
1140
+ const [isOpen, setIsOpen] = React.useState(false);
1141
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1142
+ /* @__PURE__ */ jsxRuntime.jsx(
1143
+ "button",
1144
+ {
1145
+ onClick: () => setIsOpen(true),
1146
+ className,
1147
+ style: {
1148
+ padding: "0.75rem 1.5rem",
1149
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1150
+ border: "none",
1151
+ background: "var(--nozle-primary, var(--primary))",
1152
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1153
+ cursor: "pointer",
1154
+ fontWeight: 500,
1155
+ ...style
1156
+ },
1157
+ children: label
1158
+ }
1159
+ ),
1160
+ /* @__PURE__ */ jsxRuntime.jsx(
1161
+ UpgradeModal,
1162
+ {
1163
+ isOpen,
1164
+ targetPlanId,
1165
+ customerId,
1166
+ apiBaseUrl,
1167
+ apiKey,
1168
+ onConfirm: () => {
1169
+ setIsOpen(false);
1170
+ onUpgraded?.();
1171
+ },
1172
+ onCancel: () => setIsOpen(false)
1173
+ }
1174
+ )
1175
+ ] });
1176
+ }
1177
+ var PRESET_AMOUNTS = [10, 25, 50, 100, 250];
1178
+ function CreditTopUpButton({
1179
+ label = "Add Credits",
1180
+ apiBaseUrl = "https://api.nozle.app",
1181
+ className,
1182
+ style,
1183
+ onSuccess,
1184
+ onError
1185
+ }) {
1186
+ const { customerId, apiKey } = useBillingPortal();
1187
+ const [isOpen, setIsOpen] = React.useState(false);
1188
+ const [selectedAmount, setSelectedAmount] = React.useState(25);
1189
+ const [customAmount, setCustomAmount] = React.useState("");
1190
+ const [useCustom, setUseCustom] = React.useState(false);
1191
+ const [loading, setLoading] = React.useState(false);
1192
+ const [error, setError] = React.useState(null);
1193
+ const finalAmount = useCustom ? parseFloat(customAmount) || 0 : selectedAmount;
1194
+ async function handlePurchase() {
1195
+ if (finalAmount <= 0) {
1196
+ setError("Please enter a valid amount");
1197
+ return;
1198
+ }
1199
+ setLoading(true);
1200
+ setError(null);
1201
+ try {
1202
+ const response = await fetch(`${apiBaseUrl}/api/v1/credits/purchase`, {
1203
+ method: "POST",
1204
+ headers: {
1205
+ "Content-Type": "application/json",
1206
+ Authorization: `Bearer ${apiKey}`
1207
+ },
1208
+ body: JSON.stringify({ customerId, amount: finalAmount })
1209
+ });
1210
+ if (!response.ok) {
1211
+ throw new Error(`HTTP ${response.status}: Credit purchase failed`);
1212
+ }
1213
+ setIsOpen(false);
1214
+ onSuccess?.(finalAmount);
1215
+ } catch (err) {
1216
+ const purchaseError = err instanceof Error ? err : new Error("Credit purchase failed");
1217
+ setError(purchaseError.message);
1218
+ onError?.(purchaseError);
1219
+ } finally {
1220
+ setLoading(false);
1221
+ }
1222
+ }
1223
+ const overlayStyle = {
1224
+ position: "fixed",
1225
+ inset: 0,
1226
+ background: "oklch(0 0 0 / 0.5)",
1227
+ display: "flex",
1228
+ alignItems: "center",
1229
+ justifyContent: "center",
1230
+ zIndex: 1e3
1231
+ };
1232
+ const dialogStyle = {
1233
+ background: "var(--nozle-background, var(--background))",
1234
+ color: "var(--nozle-foreground, var(--foreground))",
1235
+ border: "1px solid var(--nozle-border, var(--border))",
1236
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1237
+ padding: "2rem",
1238
+ maxWidth: "24rem",
1239
+ width: "100%"
1240
+ };
1241
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1242
+ /* @__PURE__ */ jsxRuntime.jsx(
1243
+ "button",
1244
+ {
1245
+ onClick: () => setIsOpen(true),
1246
+ className,
1247
+ style: {
1248
+ padding: "0.75rem 1.5rem",
1249
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1250
+ border: "none",
1251
+ background: "var(--nozle-primary, var(--primary))",
1252
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1253
+ cursor: "pointer",
1254
+ fontWeight: 500,
1255
+ ...style
1256
+ },
1257
+ children: label
1258
+ }
1259
+ ),
1260
+ isOpen && /* @__PURE__ */ jsxRuntime.jsx(
1261
+ "div",
1262
+ {
1263
+ style: overlayStyle,
1264
+ role: "dialog",
1265
+ "aria-modal": "true",
1266
+ "aria-label": "Add credits",
1267
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: dialogStyle, children: [
1268
+ /* @__PURE__ */ jsxRuntime.jsx(
1269
+ "h2",
1270
+ {
1271
+ style: {
1272
+ fontSize: "1.25rem",
1273
+ fontWeight: 600,
1274
+ margin: "0 0 1.25rem"
1275
+ },
1276
+ children: "Add Credits"
1277
+ }
1278
+ ),
1279
+ /* @__PURE__ */ jsxRuntime.jsx(
1280
+ "div",
1281
+ {
1282
+ style: {
1283
+ display: "flex",
1284
+ flexWrap: "wrap",
1285
+ gap: "0.5rem",
1286
+ marginBottom: "1rem"
1287
+ },
1288
+ children: PRESET_AMOUNTS.map((amount) => /* @__PURE__ */ jsxRuntime.jsxs(
1289
+ "button",
1290
+ {
1291
+ onClick: () => {
1292
+ setUseCustom(false);
1293
+ setSelectedAmount(amount);
1294
+ },
1295
+ style: {
1296
+ padding: "0.5rem 1rem",
1297
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1298
+ border: "1px solid var(--nozle-border, var(--border))",
1299
+ background: !useCustom && selectedAmount === amount ? "var(--nozle-primary, var(--primary))" : "transparent",
1300
+ color: !useCustom && selectedAmount === amount ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
1301
+ cursor: "pointer"
1302
+ },
1303
+ "aria-pressed": !useCustom && selectedAmount === amount,
1304
+ children: [
1305
+ "$",
1306
+ amount
1307
+ ]
1308
+ },
1309
+ amount
1310
+ ))
1311
+ }
1312
+ ),
1313
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem" }, children: [
1314
+ /* @__PURE__ */ jsxRuntime.jsxs(
1315
+ "label",
1316
+ {
1317
+ style: {
1318
+ display: "flex",
1319
+ alignItems: "center",
1320
+ gap: "0.5rem",
1321
+ marginBottom: "0.5rem",
1322
+ fontSize: "0.875rem",
1323
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1324
+ },
1325
+ children: [
1326
+ /* @__PURE__ */ jsxRuntime.jsx(
1327
+ "input",
1328
+ {
1329
+ type: "checkbox",
1330
+ checked: useCustom,
1331
+ onChange: (e) => setUseCustom(e.target.checked)
1332
+ }
1333
+ ),
1334
+ "Custom amount"
1335
+ ]
1336
+ }
1337
+ ),
1338
+ useCustom && /* @__PURE__ */ jsxRuntime.jsx(
1339
+ "input",
1340
+ {
1341
+ type: "number",
1342
+ value: customAmount,
1343
+ onChange: (e) => setCustomAmount(e.target.value),
1344
+ placeholder: "Enter amount",
1345
+ min: 1,
1346
+ style: {
1347
+ width: "100%",
1348
+ padding: "0.5rem 0.75rem",
1349
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1350
+ border: "1px solid var(--nozle-border, var(--border))",
1351
+ background: "var(--nozle-background, var(--background))",
1352
+ color: "var(--nozle-foreground, var(--foreground))",
1353
+ boxSizing: "border-box"
1354
+ }
1355
+ }
1356
+ )
1357
+ ] }),
1358
+ error && /* @__PURE__ */ jsxRuntime.jsx(
1359
+ "p",
1360
+ {
1361
+ style: {
1362
+ color: "oklch(0.6 0.2 25)",
1363
+ marginBottom: "1rem",
1364
+ fontSize: "0.875rem"
1365
+ },
1366
+ children: error
1367
+ }
1368
+ ),
1369
+ /* @__PURE__ */ jsxRuntime.jsxs(
1370
+ "div",
1371
+ {
1372
+ style: {
1373
+ display: "flex",
1374
+ gap: "0.75rem",
1375
+ justifyContent: "flex-end"
1376
+ },
1377
+ children: [
1378
+ /* @__PURE__ */ jsxRuntime.jsx(
1379
+ "button",
1380
+ {
1381
+ onClick: () => setIsOpen(false),
1382
+ style: {
1383
+ padding: "0.5rem 1rem",
1384
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1385
+ border: "1px solid var(--nozle-border, var(--border))",
1386
+ background: "transparent",
1387
+ color: "var(--nozle-foreground, var(--foreground))",
1388
+ cursor: "pointer"
1389
+ },
1390
+ disabled: loading,
1391
+ children: "Cancel"
1392
+ }
1393
+ ),
1394
+ /* @__PURE__ */ jsxRuntime.jsx(
1395
+ "button",
1396
+ {
1397
+ onClick: () => void handlePurchase(),
1398
+ style: {
1399
+ padding: "0.5rem 1rem",
1400
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1401
+ border: "none",
1402
+ background: "var(--nozle-primary, var(--primary))",
1403
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1404
+ cursor: loading ? "not-allowed" : "pointer",
1405
+ fontWeight: 500,
1406
+ opacity: loading ? 0.7 : 1
1407
+ },
1408
+ disabled: loading || finalAmount <= 0,
1409
+ children: loading ? "Processing..." : `Add $${finalAmount}`
1410
+ }
1411
+ )
1412
+ ]
1413
+ }
1414
+ )
1415
+ ] })
1416
+ }
1417
+ )
1418
+ ] });
1419
+ }
1420
+ function FeatureGate({
1421
+ customerId,
1422
+ feature,
1423
+ fallback = null,
1424
+ children
1425
+ }) {
1426
+ const { allowed, loading } = useCan(customerId, feature);
1427
+ if (loading) {
1428
+ return null;
1429
+ }
1430
+ if (!allowed) {
1431
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1432
+ }
1433
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1434
+ }
1435
+ function UsageGate({
1436
+ usage,
1437
+ limit,
1438
+ fallback = null,
1439
+ children
1440
+ }) {
1441
+ if (usage >= limit) {
1442
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1443
+ }
1444
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1445
+ }
1446
+ function PlanGate({
1447
+ currentPlan,
1448
+ allowedPlans,
1449
+ fallback = null,
1450
+ children
1451
+ }) {
1452
+ if (!allowedPlans.includes(currentPlan)) {
1453
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1454
+ }
1455
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1456
+ }
1457
+ function UpgradePrompt({
1458
+ text = "Upgrade to access this feature",
1459
+ planName,
1460
+ onUpgrade
1461
+ }) {
1462
+ const displayText = planName ? `Upgrade to ${planName} to access this feature` : text;
1463
+ function handleUpgrade() {
1464
+ if (onUpgrade) {
1465
+ onUpgrade();
1466
+ } else {
1467
+ window.location.href = "/upgrade";
1468
+ }
1469
+ }
1470
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1471
+ "div",
1472
+ {
1473
+ style: {
1474
+ display: "flex",
1475
+ flexDirection: "column",
1476
+ alignItems: "center",
1477
+ gap: "0.75rem",
1478
+ padding: "1rem",
1479
+ textAlign: "center"
1480
+ },
1481
+ children: [
1482
+ /* @__PURE__ */ jsxRuntime.jsx(
1483
+ "p",
1484
+ {
1485
+ style: {
1486
+ color: "var(--nozle-foreground, var(--foreground))",
1487
+ fontSize: "0.875rem",
1488
+ margin: 0
1489
+ },
1490
+ children: displayText
1491
+ }
1492
+ ),
1493
+ /* @__PURE__ */ jsxRuntime.jsx(
1494
+ "button",
1495
+ {
1496
+ onClick: handleUpgrade,
1497
+ style: {
1498
+ padding: "0.5rem 1.25rem",
1499
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1500
+ border: "none",
1501
+ background: "var(--nozle-primary, var(--primary))",
1502
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1503
+ cursor: "pointer",
1504
+ fontWeight: 500,
1505
+ fontSize: "0.875rem"
1506
+ },
1507
+ children: "Upgrade"
1508
+ }
1509
+ )
1510
+ ]
1511
+ }
1512
+ );
1513
+ }
1514
+ function LockedOverlay({
1515
+ locked,
1516
+ upgradeText,
1517
+ upgradePlanName,
1518
+ onUpgrade,
1519
+ children
1520
+ }) {
1521
+ if (!locked) {
1522
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1523
+ }
1524
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", display: "inline-block" }, children: [
1525
+ /* @__PURE__ */ jsxRuntime.jsx(
1526
+ "div",
1527
+ {
1528
+ style: {
1529
+ filter: "blur(4px)",
1530
+ pointerEvents: "none",
1531
+ userSelect: "none"
1532
+ },
1533
+ "aria-hidden": "true",
1534
+ children
1535
+ }
1536
+ ),
1537
+ /* @__PURE__ */ jsxRuntime.jsx(
1538
+ "div",
1539
+ {
1540
+ style: {
1541
+ position: "absolute",
1542
+ inset: 0,
1543
+ display: "flex",
1544
+ alignItems: "center",
1545
+ justifyContent: "center",
1546
+ background: "var(--nozle-background, var(--background))",
1547
+ opacity: 0.92,
1548
+ borderRadius: "var(--nozle-radius, 0.5rem)"
1549
+ },
1550
+ role: "dialog",
1551
+ "aria-label": "Feature locked \u2014 upgrade required",
1552
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1553
+ UpgradePrompt,
1554
+ {
1555
+ text: upgradeText,
1556
+ planName: upgradePlanName,
1557
+ onUpgrade
1558
+ }
1559
+ )
1560
+ }
1561
+ )
1562
+ ] });
1563
+ }
1564
+ function getUsageMeterColor(percentage) {
1565
+ if (percentage >= 95) return "var(--nozle-destructive, var(--destructive))";
1566
+ if (percentage >= 80) return "var(--nozle-warning, var(--warning))";
1567
+ return "var(--nozle-primary, var(--primary))";
1568
+ }
1569
+ function UsageMeter({
1570
+ used,
1571
+ limit,
1572
+ label,
1573
+ showText = true
1574
+ }) {
1575
+ const percentage = limit > 0 ? Math.min(100, used / limit * 100) : 0;
1576
+ const color = getUsageMeterColor(percentage);
1577
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full space-y-1", children: [
1578
+ label && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between text-sm", children: [
1579
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
1580
+ showText && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color }, children: [
1581
+ used.toLocaleString(),
1582
+ " / ",
1583
+ limit.toLocaleString()
1584
+ ] })
1585
+ ] }),
1586
+ /* @__PURE__ */ jsxRuntime.jsx(
1587
+ "div",
1588
+ {
1589
+ className: "h-2 rounded-full overflow-hidden",
1590
+ style: { background: "var(--nozle-border, var(--border))" },
1591
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1592
+ "div",
1593
+ {
1594
+ className: "h-full rounded-full",
1595
+ style: {
1596
+ width: `${percentage}%`,
1597
+ background: color,
1598
+ transition: "width 0.3s ease"
1599
+ }
1600
+ }
1601
+ )
1602
+ }
1603
+ )
1604
+ ] });
1605
+ }
1606
+ function UsageDashboard({
1607
+ features = [],
1608
+ loading = false
1609
+ }) {
1610
+ if (loading) {
1611
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse space-y-3", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsx(
1612
+ "div",
1613
+ {
1614
+ className: "h-8 rounded",
1615
+ style: { background: "var(--nozle-border, var(--border))" }
1616
+ },
1617
+ i
1618
+ )) });
1619
+ }
1620
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: features.map((f) => /* @__PURE__ */ jsxRuntime.jsx(UsageMeter, { used: f.used, limit: f.limit, label: f.label }, f.key)) });
1621
+ }
1622
+ function UsageAlert({
1623
+ features,
1624
+ threshold = 80,
1625
+ upgradeHref = "/plans"
1626
+ }) {
1627
+ const [dismissed, setDismissed] = React.useState(/* @__PURE__ */ new Set());
1628
+ const alertFeatures = features.filter(
1629
+ (f) => f.percentage >= threshold && !dismissed.has(f.key)
1630
+ );
1631
+ if (alertFeatures.length === 0) {
1632
+ return null;
1633
+ }
1634
+ const handleDismiss = (key) => {
1635
+ setDismissed((prev) => new Set(prev).add(key));
1636
+ };
1637
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: alertFeatures.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
1638
+ "div",
1639
+ {
1640
+ role: "alert",
1641
+ className: "flex items-center justify-between rounded-md px-4 py-3 text-sm",
1642
+ style: {
1643
+ background: "var(--nozle-warning-bg, var(--warning-bg, #fef9c3))",
1644
+ color: "var(--nozle-warning-text, var(--warning-text, #854d0e))",
1645
+ border: "1px solid var(--nozle-warning-border, var(--warning-border, #fde047))"
1646
+ },
1647
+ children: [
1648
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1649
+ /* @__PURE__ */ jsxRuntime.jsx(
1650
+ "span",
1651
+ {
1652
+ "aria-hidden": "true",
1653
+ style: { marginRight: "0.5rem", fontSize: "1rem" },
1654
+ children: "\u26A0"
1655
+ }
1656
+ ),
1657
+ "You're approaching your ",
1658
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: f.label }),
1659
+ " limit (",
1660
+ Math.round(f.percentage),
1661
+ "%).",
1662
+ " ",
1663
+ /* @__PURE__ */ jsxRuntime.jsx(
1664
+ "a",
1665
+ {
1666
+ href: upgradeHref,
1667
+ style: {
1668
+ color: "inherit",
1669
+ fontWeight: 600,
1670
+ textDecoration: "underline"
1671
+ },
1672
+ children: "Upgrade"
1673
+ }
1674
+ )
1675
+ ] }),
1676
+ /* @__PURE__ */ jsxRuntime.jsx(
1677
+ "button",
1678
+ {
1679
+ onClick: () => handleDismiss(f.key),
1680
+ "aria-label": `Dismiss ${f.label} alert`,
1681
+ style: {
1682
+ background: "none",
1683
+ border: "none",
1684
+ cursor: "pointer",
1685
+ padding: "0 0 0 0.75rem",
1686
+ fontSize: "1rem",
1687
+ color: "inherit"
1688
+ },
1689
+ children: "\xD7"
1690
+ }
1691
+ )
1692
+ ]
1693
+ },
1694
+ f.key
1695
+ )) });
1696
+ }
1697
+ function CreditBalance({
1698
+ customerId,
1699
+ currency = "USD"
1700
+ }) {
1701
+ const { balance, loading, error } = useCredits(customerId);
1702
+ if (loading) {
1703
+ return /* @__PURE__ */ jsxRuntime.jsx(
1704
+ "div",
1705
+ {
1706
+ className: "animate-pulse h-8 w-24 rounded",
1707
+ style: { background: "var(--nozle-border, var(--border))" }
1708
+ }
1709
+ );
1710
+ }
1711
+ if (error) {
1712
+ return /* @__PURE__ */ jsxRuntime.jsx(
1713
+ "span",
1714
+ {
1715
+ style: { color: "var(--nozle-destructive, var(--destructive))" },
1716
+ className: "text-sm",
1717
+ children: "Failed to load balance"
1718
+ }
1719
+ );
1720
+ }
1721
+ const amount = balance ?? 0;
1722
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1723
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "wallet icon", style: { fontSize: "1.25rem" }, role: "img", children: "\u{1F4B3}" }),
1724
+ amount === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1725
+ "span",
1726
+ {
1727
+ style: {
1728
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1729
+ },
1730
+ className: "text-sm",
1731
+ children: "No credits"
1732
+ }
1733
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold", children: new Intl.NumberFormat("en-US", {
1734
+ style: "currency",
1735
+ currency,
1736
+ minimumFractionDigits: 2
1737
+ }).format(amount) })
1738
+ ] });
1739
+ }
1740
+ var TYPE_BADGE_STYLES = {
1741
+ grant: {
1742
+ background: "var(--nozle-success-bg, #dcfce7)",
1743
+ color: "var(--nozle-success, #16a34a)"
1744
+ },
1745
+ deduct: {
1746
+ background: "var(--nozle-destructive-bg, #fee2e2)",
1747
+ color: "var(--nozle-destructive, #dc2626)"
1748
+ },
1749
+ purchase: {
1750
+ background: "var(--nozle-info-bg, #dbeafe)",
1751
+ color: "var(--nozle-info, #2563eb)"
1752
+ },
1753
+ application: {
1754
+ background: "var(--nozle-muted, #f1f5f9)",
1755
+ color: "var(--nozle-muted-foreground, #64748b)"
1756
+ }
1757
+ };
1758
+ function getTypeBadgeStyle(type) {
1759
+ return TYPE_BADGE_STYLES[type] ?? TYPE_BADGE_STYLES.application;
1760
+ }
1761
+ function formatDate(iso) {
1762
+ try {
1763
+ return new Date(iso).toLocaleDateString("en-US", {
1764
+ year: "numeric",
1765
+ month: "short",
1766
+ day: "numeric"
1767
+ });
1768
+ } catch {
1769
+ return iso;
1770
+ }
1771
+ }
1772
+ function CreditHistory({
1773
+ customerId,
1774
+ viewAllHref = "/billing/credits"
1775
+ }) {
1776
+ const { client } = useBillingContext();
1777
+ const [transactions, setTransactions] = React.useState([]);
1778
+ const [loading, setLoading] = React.useState(true);
1779
+ const [error, setError] = React.useState(null);
1780
+ React.useEffect(() => {
1781
+ if (!client) {
1782
+ setError("No client in context");
1783
+ setLoading(false);
1784
+ return;
1785
+ }
1786
+ let cancelled = false;
1787
+ async function fetchHistory() {
1788
+ try {
1789
+ const clientInternal = client;
1790
+ const apiKey = clientInternal.apiKey ?? "";
1791
+ const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
1792
+ const timeout = clientInternal.timeout ?? 5e3;
1793
+ const controller = new AbortController();
1794
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1795
+ const response = await fetch(
1796
+ `${baseUrl}/api/v1/credits/${customerId}/transactions`,
1797
+ {
1798
+ headers: { Authorization: `Bearer ${apiKey}` },
1799
+ signal: controller.signal
1800
+ }
1801
+ );
1802
+ clearTimeout(timeoutId);
1803
+ if (!response.ok) {
1804
+ throw new Error(`HTTP ${response.status}`);
1805
+ }
1806
+ const data = await response.json();
1807
+ if (!cancelled) {
1808
+ setTransactions(
1809
+ (data.transactions ?? []).slice().sort(
1810
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
1811
+ )
1812
+ );
1813
+ setLoading(false);
1814
+ }
1815
+ } catch (err) {
1816
+ if (!cancelled) {
1817
+ setError(err instanceof Error ? err.message : "Unknown error");
1818
+ setLoading(false);
1819
+ }
1820
+ }
1821
+ }
1822
+ void fetchHistory();
1823
+ return () => {
1824
+ cancelled = true;
1825
+ };
1826
+ }, [client, customerId]);
1827
+ if (loading) {
1828
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsx(
1829
+ "div",
1830
+ {
1831
+ className: "h-6 rounded",
1832
+ style: { background: "var(--nozle-border, var(--border))" }
1833
+ },
1834
+ i
1835
+ )) });
1836
+ }
1837
+ if (error) {
1838
+ return /* @__PURE__ */ jsxRuntime.jsx(
1839
+ "span",
1840
+ {
1841
+ style: { color: "var(--nozle-destructive, var(--destructive))" },
1842
+ className: "text-sm",
1843
+ children: "Failed to load transactions"
1844
+ }
1845
+ );
1846
+ }
1847
+ const displayed = transactions.slice(0, 20);
1848
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1849
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
1850
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1851
+ "tr",
1852
+ {
1853
+ style: {
1854
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
1855
+ fontSize: "0.75rem",
1856
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1857
+ },
1858
+ children: [
1859
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.25rem 0.5rem" }, children: "Type" }),
1860
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "right", padding: "0.25rem 0.5rem" }, children: "Amount" }),
1861
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.25rem 0.5rem" }, children: "Description" }),
1862
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "right", padding: "0.25rem 0.5rem" }, children: "Date" })
1863
+ ]
1864
+ }
1865
+ ) }),
1866
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: displayed.map((tx) => /* @__PURE__ */ jsxRuntime.jsxs(
1867
+ "tr",
1868
+ {
1869
+ style: {
1870
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
1871
+ fontSize: "0.875rem"
1872
+ },
1873
+ children: [
1874
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1875
+ "span",
1876
+ {
1877
+ style: {
1878
+ ...getTypeBadgeStyle(tx.type),
1879
+ padding: "0.125rem 0.5rem",
1880
+ borderRadius: "9999px",
1881
+ fontSize: "0.75rem",
1882
+ fontWeight: 500
1883
+ },
1884
+ children: tx.type
1885
+ }
1886
+ ) }),
1887
+ /* @__PURE__ */ jsxRuntime.jsxs(
1888
+ "td",
1889
+ {
1890
+ style: {
1891
+ textAlign: "right",
1892
+ padding: "0.5rem",
1893
+ fontVariantNumeric: "tabular-nums",
1894
+ color: tx.type === "deduct" ? "var(--nozle-destructive, var(--destructive))" : "var(--nozle-success, #16a34a)"
1895
+ },
1896
+ children: [
1897
+ tx.type === "deduct" ? "-" : "+",
1898
+ Math.abs(tx.amount).toLocaleString()
1899
+ ]
1900
+ }
1901
+ ),
1902
+ /* @__PURE__ */ jsxRuntime.jsx(
1903
+ "td",
1904
+ {
1905
+ style: {
1906
+ padding: "0.5rem",
1907
+ color: "var(--nozle-foreground, var(--foreground))"
1908
+ },
1909
+ children: tx.description
1910
+ }
1911
+ ),
1912
+ /* @__PURE__ */ jsxRuntime.jsx(
1913
+ "td",
1914
+ {
1915
+ style: {
1916
+ textAlign: "right",
1917
+ padding: "0.5rem",
1918
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
1919
+ fontSize: "0.75rem"
1920
+ },
1921
+ children: formatDate(tx.createdAt)
1922
+ }
1923
+ )
1924
+ ]
1925
+ },
1926
+ tx.id
1927
+ )) })
1928
+ ] }),
1929
+ transactions.length > 20 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "right" }, children: /* @__PURE__ */ jsxRuntime.jsxs(
1930
+ "a",
1931
+ {
1932
+ href: viewAllHref,
1933
+ style: {
1934
+ color: "var(--nozle-primary, var(--primary))",
1935
+ fontSize: "0.875rem",
1936
+ textDecoration: "underline"
1937
+ },
1938
+ children: [
1939
+ "View all (",
1940
+ transactions.length,
1941
+ ")"
1942
+ ]
1943
+ }
1944
+ ) }),
1945
+ displayed.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(
1946
+ "p",
1947
+ {
1948
+ style: {
1949
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
1950
+ fontSize: "0.875rem",
1951
+ textAlign: "center",
1952
+ padding: "1rem"
1953
+ },
1954
+ children: "No credit transactions yet."
1955
+ }
1956
+ )
1957
+ ] });
1958
+ }
1959
+ var TIER_STYLES = {
1960
+ free: {
1961
+ background: "var(--nozle-muted, var(--muted, #f1f5f9))",
1962
+ color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
1963
+ border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
1964
+ },
1965
+ starter: {
1966
+ background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
1967
+ color: "var(--nozle-primary, var(--primary, #3b82f6))",
1968
+ border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
1969
+ },
1970
+ pro: {
1971
+ background: "var(--nozle-primary, var(--primary, #3b82f6))",
1972
+ color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
1973
+ border: "1px solid transparent"
1974
+ },
1975
+ enterprise: {
1976
+ background: "var(--nozle-accent, var(--accent, #8b5cf6))",
1977
+ color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
1978
+ border: "1px solid transparent"
1979
+ }
1980
+ };
1981
+ function PlanBadge({ plan, tier = "free" }) {
1982
+ const style = TIER_STYLES[tier] ?? TIER_STYLES.free;
1983
+ return /* @__PURE__ */ jsxRuntime.jsx(
1984
+ "span",
1985
+ {
1986
+ style: {
1987
+ ...style,
1988
+ display: "inline-flex",
1989
+ alignItems: "center",
1990
+ padding: "0.125rem 0.5rem",
1991
+ borderRadius: "9999px",
1992
+ fontSize: "0.75rem",
1993
+ fontWeight: 600,
1994
+ lineHeight: "1.25rem",
1995
+ whiteSpace: "nowrap"
1996
+ },
1997
+ children: plan
1998
+ }
1999
+ );
2000
+ }
2001
+ function formatDate2(iso) {
2002
+ if (!iso) return "\u2014";
2003
+ try {
2004
+ return new Date(iso).toLocaleDateString("en-US", {
2005
+ year: "numeric",
2006
+ month: "long",
2007
+ day: "numeric"
2008
+ });
2009
+ } catch {
2010
+ return iso;
2011
+ }
2012
+ }
2013
+ function CurrentPlan({
2014
+ customerId,
2015
+ onChangePlan,
2016
+ changePlanHref = "/plans"
2017
+ }) {
2018
+ const { client } = useBillingContext();
2019
+ const [plan, setPlan] = React.useState(null);
2020
+ const [loading, setLoading] = React.useState(true);
2021
+ const [error, setError] = React.useState(null);
2022
+ React.useEffect(() => {
2023
+ if (!client) {
2024
+ setError("No client in context");
2025
+ setLoading(false);
2026
+ return;
2027
+ }
2028
+ let cancelled = false;
2029
+ async function fetchPlan() {
2030
+ try {
2031
+ const clientInternal = client;
2032
+ const apiKey = clientInternal.apiKey ?? "";
2033
+ const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
2034
+ const timeout = clientInternal.timeout ?? 5e3;
2035
+ const controller = new AbortController();
2036
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
2037
+ const response = await fetch(
2038
+ `${baseUrl}/api/v1/subscriptions/current?customer_id=${customerId}`,
2039
+ {
2040
+ headers: { Authorization: `Bearer ${apiKey}` },
2041
+ signal: controller.signal
2042
+ }
2043
+ );
2044
+ clearTimeout(timeoutId);
2045
+ if (!response.ok) {
2046
+ throw new Error(`HTTP ${response.status}`);
2047
+ }
2048
+ const data = await response.json();
2049
+ if (!cancelled) {
2050
+ setPlan(data);
2051
+ setLoading(false);
2052
+ }
2053
+ } catch (err) {
2054
+ if (!cancelled) {
2055
+ setError(err instanceof Error ? err.message : "Unknown error");
2056
+ setLoading(false);
2057
+ }
2058
+ }
2059
+ }
2060
+ void fetchPlan();
2061
+ return () => {
2062
+ cancelled = true;
2063
+ };
2064
+ }, [client, customerId]);
2065
+ if (loading) {
2066
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse space-y-2", children: [
2067
+ /* @__PURE__ */ jsxRuntime.jsx(
2068
+ "div",
2069
+ {
2070
+ className: "h-6 w-32 rounded",
2071
+ style: { background: "var(--nozle-border, var(--border))" }
2072
+ }
2073
+ ),
2074
+ /* @__PURE__ */ jsxRuntime.jsx(
2075
+ "div",
2076
+ {
2077
+ className: "h-4 w-48 rounded",
2078
+ style: { background: "var(--nozle-border, var(--border))" }
2079
+ }
2080
+ )
2081
+ ] });
2082
+ }
2083
+ if (error || !plan) {
2084
+ return /* @__PURE__ */ jsxRuntime.jsx(
2085
+ "span",
2086
+ {
2087
+ style: { color: "var(--nozle-destructive, var(--destructive))" },
2088
+ className: "text-sm",
2089
+ children: "Failed to load current plan"
2090
+ }
2091
+ );
2092
+ }
2093
+ const handleChangePlan = () => {
2094
+ if (onChangePlan) {
2095
+ onChangePlan();
2096
+ } else {
2097
+ window.location.href = changePlanHref;
2098
+ }
2099
+ };
2100
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2101
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
2102
+ /* @__PURE__ */ jsxRuntime.jsx(PlanBadge, { plan: plan.planName, tier: plan.tier }),
2103
+ /* @__PURE__ */ jsxRuntime.jsx(
2104
+ "span",
2105
+ {
2106
+ style: {
2107
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2108
+ fontSize: "0.875rem"
2109
+ },
2110
+ children: plan.interval
2111
+ }
2112
+ ),
2113
+ /* @__PURE__ */ jsxRuntime.jsx(
2114
+ "span",
2115
+ {
2116
+ style: {
2117
+ color: plan.status === "active" ? "var(--nozle-success, #16a34a)" : "var(--nozle-warning, var(--warning))",
2118
+ fontSize: "0.875rem",
2119
+ fontWeight: 500
2120
+ },
2121
+ children: plan.status
2122
+ }
2123
+ )
2124
+ ] }),
2125
+ plan.nextBillingDate && /* @__PURE__ */ jsxRuntime.jsxs(
2126
+ "p",
2127
+ {
2128
+ style: {
2129
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2130
+ fontSize: "0.875rem"
2131
+ },
2132
+ children: [
2133
+ "Next billing: ",
2134
+ formatDate2(plan.nextBillingDate)
2135
+ ]
2136
+ }
2137
+ ),
2138
+ /* @__PURE__ */ jsxRuntime.jsx(
2139
+ "button",
2140
+ {
2141
+ onClick: handleChangePlan,
2142
+ style: {
2143
+ background: "transparent",
2144
+ border: "1px solid var(--nozle-primary, var(--primary))",
2145
+ color: "var(--nozle-primary, var(--primary))",
2146
+ padding: "0.375rem 0.875rem",
2147
+ borderRadius: "0.375rem",
2148
+ fontSize: "0.875rem",
2149
+ fontWeight: 500,
2150
+ cursor: "pointer"
2151
+ },
2152
+ children: "Change Plan"
2153
+ }
2154
+ )
2155
+ ] });
2156
+ }
2157
+ var STATUS_STYLES = {
2158
+ paid: {
2159
+ background: "var(--nozle-success-bg, #dcfce7)",
2160
+ color: "var(--nozle-success, #16a34a)"
2161
+ },
2162
+ open: {
2163
+ background: "var(--nozle-info-bg, #dbeafe)",
2164
+ color: "var(--nozle-info, #2563eb)"
2165
+ },
2166
+ draft: {
2167
+ background: "var(--nozle-muted, #f1f5f9)",
2168
+ color: "var(--nozle-muted-foreground, #64748b)"
2169
+ },
2170
+ void: {
2171
+ background: "var(--nozle-muted, #f1f5f9)",
2172
+ color: "var(--nozle-muted-foreground, #64748b)"
2173
+ },
2174
+ uncollectible: {
2175
+ background: "var(--nozle-destructive-bg, #fee2e2)",
2176
+ color: "var(--nozle-destructive, #dc2626)"
2177
+ }
2178
+ };
2179
+ function formatDate3(iso) {
2180
+ try {
2181
+ return new Date(iso).toLocaleDateString("en-US", {
2182
+ year: "numeric",
2183
+ month: "short",
2184
+ day: "numeric"
2185
+ });
2186
+ } catch {
2187
+ return iso;
2188
+ }
2189
+ }
2190
+ function InvoiceList({ customerId }) {
2191
+ const { client } = useBillingContext();
2192
+ const [invoices, setInvoices] = React.useState([]);
2193
+ const [loading, setLoading] = React.useState(true);
2194
+ const [error, setError] = React.useState(null);
2195
+ React.useEffect(() => {
2196
+ if (!client) {
2197
+ setError("No client in context");
2198
+ setLoading(false);
2199
+ return;
369
2200
  }
370
- if (variant === "ring") {
371
- return /* @__PURE__ */ jsxRuntime.jsx(
372
- "div",
373
- {
374
- "data-nozle": "usage-meter",
375
- "data-variant": "ring",
376
- className,
377
- style: {
378
- width: 96,
379
- height: 96,
380
- borderRadius: "50%",
381
- background: "#e5e7eb",
382
- animation: "nozle-pulse 1.5s ease-in-out infinite",
383
- ...style
384
- },
385
- ...props
2201
+ let cancelled = false;
2202
+ async function fetchInvoices() {
2203
+ try {
2204
+ const clientInternal = client;
2205
+ const apiKey = clientInternal.apiKey ?? "";
2206
+ const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
2207
+ const timeout = clientInternal.timeout ?? 5e3;
2208
+ const controller = new AbortController();
2209
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
2210
+ const response = await fetch(
2211
+ `${baseUrl}/api/v1/invoices?customer_id=${customerId}`,
2212
+ {
2213
+ headers: { Authorization: `Bearer ${apiKey}` },
2214
+ signal: controller.signal
2215
+ }
2216
+ );
2217
+ clearTimeout(timeoutId);
2218
+ if (!response.ok) {
2219
+ throw new Error(`HTTP ${response.status}`);
386
2220
  }
387
- );
2221
+ const data = await response.json();
2222
+ if (!cancelled) {
2223
+ setInvoices(data.invoices ?? data.data ?? []);
2224
+ setLoading(false);
2225
+ }
2226
+ } catch (err) {
2227
+ if (!cancelled) {
2228
+ setError(err instanceof Error ? err.message : "Unknown error");
2229
+ setLoading(false);
2230
+ }
2231
+ }
388
2232
  }
389
- return /* @__PURE__ */ jsxRuntime.jsx(
2233
+ void fetchInvoices();
2234
+ return () => {
2235
+ cancelled = true;
2236
+ };
2237
+ }, [client, customerId]);
2238
+ if (loading) {
2239
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsx(
390
2240
  "div",
391
2241
  {
392
- "data-nozle": "usage-meter",
393
- "data-variant": "bar",
394
- className,
2242
+ className: "h-8 rounded",
2243
+ style: { background: "var(--nozle-border, var(--border))" }
2244
+ },
2245
+ i
2246
+ )) });
2247
+ }
2248
+ if (error) {
2249
+ return /* @__PURE__ */ jsxRuntime.jsx(
2250
+ "span",
2251
+ {
2252
+ style: { color: "var(--nozle-destructive, var(--destructive))" },
2253
+ className: "text-sm",
2254
+ children: "Failed to load invoices"
2255
+ }
2256
+ );
2257
+ }
2258
+ if (invoices.length === 0) {
2259
+ return /* @__PURE__ */ jsxRuntime.jsx(
2260
+ "p",
2261
+ {
395
2262
  style: {
396
- maxWidth: 360,
397
- fontFamily,
398
- ...style
2263
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2264
+ fontSize: "0.875rem",
2265
+ textAlign: "center",
2266
+ padding: "1.5rem"
399
2267
  },
400
- ...props,
401
- children: /* @__PURE__ */ jsxRuntime.jsx(
402
- "div",
403
- {
404
- style: {
405
- height: 8,
406
- borderRadius: 4,
407
- background: "#e5e7eb",
408
- animation: "nozle-pulse 1.5s ease-in-out infinite"
409
- }
410
- }
411
- )
2268
+ children: "No invoices yet."
412
2269
  }
413
2270
  );
414
2271
  }
415
- if (error || !data) return null;
416
- const percent = data.limit > 0 ? Math.round(data.used / data.limit * 100) : 0;
417
- const level = percent >= 90 ? "critical" : percent >= 75 ? "warning" : "normal";
418
- const colors = levelColors[level];
419
- if (variant === "minimal") {
420
- const minimalStyle = {
421
- fontFamily,
422
- fontSize: 13,
423
- fontWeight: 500,
424
- color: colors.fg,
425
- background: colors.bg,
426
- padding: "3px 8px",
427
- borderRadius: 6,
428
- whiteSpace: "nowrap",
429
- ...style
430
- };
431
- return /* @__PURE__ */ jsxRuntime.jsxs(
432
- "span",
2272
+ return /* @__PURE__ */ jsxRuntime.jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
2273
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs(
2274
+ "tr",
433
2275
  {
434
- "data-nozle": "usage-meter",
435
- "data-variant": "minimal",
436
- "data-level": level,
437
- className,
438
- style: minimalStyle,
439
- ...props,
2276
+ style: {
2277
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
2278
+ fontSize: "0.75rem",
2279
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2280
+ },
440
2281
  children: [
441
- data.used.toLocaleString(),
442
- "/",
443
- data.limit.toLocaleString()
2282
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem" }, children: "#" }),
2283
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem" }, children: "Date" }),
2284
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "right", padding: "0.5rem" }, children: "Amount" }),
2285
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "center", padding: "0.5rem" }, children: "Status" }),
2286
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "center", padding: "0.5rem" }, children: "PDF" })
444
2287
  ]
445
2288
  }
446
- );
447
- }
448
- if (variant === "ring") {
449
- const size = 96;
450
- const strokeWidth = 7;
451
- const radius = (size - strokeWidth) / 2;
452
- const circumference = 2 * Math.PI * radius;
453
- const offset = circumference - percent / 100 * circumference;
454
- const containerStyle = {
455
- position: "relative",
456
- display: "inline-flex",
457
- alignItems: "center",
458
- justifyContent: "center",
459
- width: size,
460
- height: size,
461
- fontFamily,
462
- ...style
463
- };
464
- const labelStyle2 = {
465
- position: "absolute",
466
- inset: 0,
467
- display: "flex",
468
- flexDirection: "column",
469
- alignItems: "center",
470
- justifyContent: "center",
471
- fontWeight: 700,
472
- fontSize: 20,
473
- lineHeight: 1,
474
- color: colors.fg
475
- };
476
- return /* @__PURE__ */ jsxRuntime.jsxs(
477
- "div",
2289
+ ) }),
2290
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: invoices.map((inv) => /* @__PURE__ */ jsxRuntime.jsxs(
2291
+ "tr",
478
2292
  {
479
- "data-nozle": "usage-meter",
480
- "data-variant": "ring",
481
- "data-level": level,
482
- className,
483
- style: containerStyle,
484
- ...props,
2293
+ style: {
2294
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
2295
+ fontSize: "0.875rem"
2296
+ },
485
2297
  children: [
486
- /* @__PURE__ */ jsxRuntime.jsxs(
487
- "svg",
2298
+ /* @__PURE__ */ jsxRuntime.jsx(
2299
+ "td",
488
2300
  {
489
- viewBox: `0 0 ${size} ${size}`,
490
- width: size,
491
- height: size,
492
- style: { display: "block", transform: "rotate(-90deg)" },
493
- children: [
494
- /* @__PURE__ */ jsxRuntime.jsx(
495
- "circle",
496
- {
497
- cx: size / 2,
498
- cy: size / 2,
499
- r: radius,
500
- fill: "none",
501
- stroke: colors.trackBg,
502
- strokeWidth
503
- }
504
- ),
505
- /* @__PURE__ */ jsxRuntime.jsx(
506
- "circle",
507
- {
508
- cx: size / 2,
509
- cy: size / 2,
510
- r: radius,
511
- fill: "none",
512
- stroke: colors.fg,
513
- strokeWidth,
514
- strokeDasharray: circumference,
515
- strokeDashoffset: offset,
516
- strokeLinecap: "round",
517
- style: { transition: "stroke-dashoffset 0.4s ease" }
518
- }
519
- )
520
- ]
2301
+ style: {
2302
+ padding: "0.5rem",
2303
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2304
+ },
2305
+ children: inv.number
521
2306
  }
522
2307
  ),
523
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-nozle": "usage-meter-label", style: labelStyle2, children: [
524
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
525
- percent,
526
- "%"
527
- ] }),
528
- /* @__PURE__ */ jsxRuntime.jsx(
529
- "span",
530
- {
531
- style: {
532
- fontSize: 10,
533
- fontWeight: 400,
534
- color: "#6b7280",
535
- marginTop: 2
536
- },
537
- children: "used"
538
- }
539
- )
540
- ] })
2308
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem" }, children: formatDate3(inv.date) }),
2309
+ /* @__PURE__ */ jsxRuntime.jsx(
2310
+ "td",
2311
+ {
2312
+ style: {
2313
+ textAlign: "right",
2314
+ padding: "0.5rem",
2315
+ fontVariantNumeric: "tabular-nums"
2316
+ },
2317
+ children: new Intl.NumberFormat("en-US", {
2318
+ style: "currency",
2319
+ currency: inv.currency ?? "USD"
2320
+ }).format(inv.amount / 100)
2321
+ }
2322
+ ),
2323
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { textAlign: "center", padding: "0.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2324
+ "span",
2325
+ {
2326
+ style: {
2327
+ ...STATUS_STYLES[inv.status] ?? STATUS_STYLES.draft,
2328
+ padding: "0.125rem 0.5rem",
2329
+ borderRadius: "9999px",
2330
+ fontSize: "0.75rem",
2331
+ fontWeight: 500
2332
+ },
2333
+ children: inv.status
2334
+ }
2335
+ ) }),
2336
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { textAlign: "center", padding: "0.5rem" }, children: inv.pdf_url ? /* @__PURE__ */ jsxRuntime.jsx(
2337
+ "a",
2338
+ {
2339
+ href: inv.pdf_url,
2340
+ target: "_blank",
2341
+ rel: "noopener noreferrer",
2342
+ "aria-label": `Download invoice ${inv.number}`,
2343
+ style: {
2344
+ color: "var(--nozle-primary, var(--primary))",
2345
+ textDecoration: "underline",
2346
+ fontSize: "0.75rem"
2347
+ },
2348
+ children: "Download"
2349
+ }
2350
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2351
+ "span",
2352
+ {
2353
+ style: {
2354
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2355
+ fontSize: "0.75rem"
2356
+ },
2357
+ children: "\u2014"
2358
+ }
2359
+ ) })
541
2360
  ]
542
- }
543
- );
544
- }
545
- const cardStyle = {
546
- fontFamily,
547
- maxWidth: 360,
548
- background: "#ffffff",
549
- border: "1px solid #e5e7eb",
550
- borderRadius: 12,
551
- padding: "16px 20px",
552
- boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
553
- ...style
554
- };
555
- const headerStyle = {
556
- display: "flex",
557
- justifyContent: "space-between",
558
- alignItems: "baseline",
559
- marginBottom: 10
560
- };
561
- const labelStyle = {
562
- fontSize: 13,
563
- fontWeight: 500,
564
- color: "#374151"
565
- };
566
- const valueStyle = {
567
- fontSize: 13,
568
- fontWeight: 600,
569
- color: colors.fg
570
- };
571
- const trackStyle = {
572
- height: 8,
573
- borderRadius: 4,
574
- background: colors.trackBg,
575
- overflow: "hidden"
576
- };
577
- const fillStyle = {
578
- height: "100%",
579
- borderRadius: 4,
580
- background: colors.fg,
581
- width: `${Math.min(percent, 100)}%`,
582
- transition: "width 0.4s ease"
583
- };
2361
+ },
2362
+ inv.id
2363
+ )) })
2364
+ ] });
2365
+ }
2366
+ function Section({
2367
+ title,
2368
+ children
2369
+ }) {
584
2370
  return /* @__PURE__ */ jsxRuntime.jsxs(
585
2371
  "div",
586
2372
  {
587
- "data-nozle": "usage-meter",
588
- "data-variant": "bar",
589
- "data-level": level,
590
- className,
591
- style: cardStyle,
592
- ...props,
2373
+ style: {
2374
+ padding: "1.25rem",
2375
+ border: "1px solid var(--nozle-border, var(--border))",
2376
+ borderRadius: "0.75rem",
2377
+ background: "var(--nozle-card, var(--card, #ffffff))"
2378
+ },
593
2379
  children: [
594
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headerStyle, children: [
595
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-nozle": "usage-meter-label", style: labelStyle, children: [
596
- data.used.toLocaleString(),
597
- " / ",
598
- data.limit.toLocaleString()
599
- ] }),
600
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: valueStyle, children: [
601
- percent,
602
- "%"
603
- ] })
604
- ] }),
605
- /* @__PURE__ */ jsxRuntime.jsx("div", { "data-nozle": "usage-meter-track", style: trackStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-nozle": "usage-meter-fill", style: fillStyle }) })
2380
+ /* @__PURE__ */ jsxRuntime.jsx(
2381
+ "h3",
2382
+ {
2383
+ style: {
2384
+ fontSize: "0.875rem",
2385
+ fontWeight: 600,
2386
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2387
+ textTransform: "uppercase",
2388
+ letterSpacing: "0.05em",
2389
+ marginBottom: "1rem"
2390
+ },
2391
+ children: title
2392
+ }
2393
+ ),
2394
+ children
606
2395
  ]
607
2396
  }
608
2397
  );
609
2398
  }
610
- var fontFamily2 = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
611
- var statusColorMap = {
612
- active: { dot: "#22c55e", bg: "rgba(34, 197, 94, 0.08)", text: "#15803d" },
613
- trialing: { dot: "#6366f1", bg: "rgba(99, 102, 241, 0.08)", text: "#4f46e5" },
614
- past_due: { dot: "#f59e0b", bg: "rgba(245, 158, 11, 0.08)", text: "#b45309" },
615
- canceled: { dot: "#6b7280", bg: "rgba(107, 114, 128, 0.08)", text: "#4b5563" },
616
- unpaid: { dot: "#ef4444", bg: "rgba(239, 68, 68, 0.08)", text: "#dc2626" },
617
- incomplete: { dot: "#f59e0b", bg: "rgba(245, 158, 11, 0.08)", text: "#b45309" }
618
- };
619
- var defaultStatusColors = { dot: "#6366f1", bg: "rgba(99, 102, 241, 0.08)", text: "#4f46e5" };
620
- function getStatusColors(status) {
621
- return statusColorMap[status] ?? defaultStatusColors;
622
- }
623
- var skeletonStyleInjected2 = false;
624
- function ensureKeyframes2() {
625
- if (skeletonStyleInjected2 || typeof document === "undefined") return;
626
- const sheet = document.createElement("style");
627
- sheet.textContent = `@keyframes nozle-pulse{0%,100%{opacity:1}50%{opacity:.4}}`;
628
- document.head.appendChild(sheet);
629
- skeletonStyleInjected2 = true;
630
- }
631
- function PlanBadge({
632
- variant = "pill",
633
- className,
634
- style,
635
- ...props
2399
+ function BillingPortal({
2400
+ customerId,
2401
+ usageFeatures,
2402
+ usageLoading = false,
2403
+ onChangePlan
636
2404
  }) {
637
- const { data, isLoading, error } = usePlan();
638
- if (isLoading) {
639
- ensureKeyframes2();
640
- if (variant === "icon") {
641
- return /* @__PURE__ */ jsxRuntime.jsx(
642
- "span",
643
- {
644
- "data-nozle": "plan-badge",
645
- "data-variant": "icon",
646
- className,
647
- style: {
648
- display: "inline-block",
649
- width: 32,
650
- height: 32,
651
- borderRadius: "50%",
652
- background: "#e5e7eb",
653
- animation: "nozle-pulse 1.5s ease-in-out infinite",
654
- ...style
655
- },
656
- ...props
657
- }
658
- );
659
- }
660
- return /* @__PURE__ */ jsxRuntime.jsx(
661
- "span",
662
- {
663
- "data-nozle": "plan-badge",
664
- "data-variant": variant,
665
- className,
666
- style: {
667
- display: "inline-block",
668
- width: 72,
669
- height: 22,
670
- borderRadius: variant === "pill" ? 999 : 4,
671
- background: "#e5e7eb",
672
- animation: "nozle-pulse 1.5s ease-in-out infinite",
673
- ...style
674
- },
675
- ...props
676
- }
677
- );
678
- }
679
- if (error || !data) return null;
680
- const statusLabel = data.subscription_status.replace(/_/g, " ");
681
- const colors = getStatusColors(data.subscription_status);
682
- const planName = data.plan_slug;
683
- if (variant === "text") {
684
- const textStyle = {
685
- fontFamily: fontFamily2,
686
- display: "inline-flex",
687
- alignItems: "center",
688
- gap: 6,
689
- fontSize: 14,
690
- fontWeight: 500,
691
- color: "#111827",
692
- ...style
693
- };
694
- const dotStyle2 = {
695
- width: 8,
696
- height: 8,
697
- borderRadius: "50%",
698
- background: colors.dot,
699
- flexShrink: 0
700
- };
701
- return /* @__PURE__ */ jsxRuntime.jsxs(
702
- "span",
703
- {
704
- "data-nozle": "plan-badge",
705
- "data-variant": "text",
706
- "data-status": data.subscription_status,
707
- className,
708
- style: textStyle,
709
- ...props,
710
- children: [
711
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: dotStyle2, "aria-hidden": "true" }),
712
- planName
713
- ]
714
- }
715
- );
716
- }
717
- if (variant === "icon") {
718
- const iconStyle = {
719
- fontFamily: fontFamily2,
720
- display: "inline-flex",
721
- alignItems: "center",
722
- justifyContent: "center",
723
- width: 32,
724
- height: 32,
725
- borderRadius: "50%",
726
- background: colors.bg,
727
- color: colors.text,
728
- fontSize: 14,
729
- fontWeight: 700,
730
- lineHeight: 1,
731
- flexShrink: 0,
732
- ...style
733
- };
734
- return /* @__PURE__ */ jsxRuntime.jsx(
735
- "span",
736
- {
737
- "data-nozle": "plan-badge",
738
- "data-variant": "icon",
739
- "data-status": data.subscription_status,
740
- className,
741
- style: iconStyle,
742
- title: `${planName} (${statusLabel})`,
743
- ...props,
744
- children: planName.charAt(0).toUpperCase()
745
- }
746
- );
747
- }
748
- const pillStyle = {
749
- fontFamily: fontFamily2,
750
- display: "inline-flex",
751
- alignItems: "center",
752
- gap: 6,
753
- fontSize: 12,
754
- fontWeight: 600,
755
- lineHeight: 1.4,
756
- color: colors.text,
757
- background: colors.bg,
758
- padding: "4px 12px",
759
- borderRadius: 999,
760
- whiteSpace: "nowrap",
761
- ...style
762
- };
763
- const dotStyle = {
764
- width: 6,
765
- height: 6,
766
- borderRadius: "50%",
767
- background: colors.dot,
768
- flexShrink: 0
769
- };
770
2405
  return /* @__PURE__ */ jsxRuntime.jsxs(
771
- "span",
2406
+ "div",
772
2407
  {
773
- "data-nozle": "plan-badge",
774
- "data-variant": "pill",
775
- "data-status": data.subscription_status,
776
- className,
777
- style: pillStyle,
778
- ...props,
2408
+ style: {
2409
+ display: "flex",
2410
+ flexDirection: "column",
2411
+ gap: "1.25rem",
2412
+ fontFamily: "var(--nozle-font-sans, var(--font-sans, system-ui, sans-serif))",
2413
+ color: "var(--nozle-foreground, var(--foreground))"
2414
+ },
779
2415
  children: [
780
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: dotStyle, "aria-hidden": "true" }),
781
- planName
2416
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Current Plan", children: /* @__PURE__ */ jsxRuntime.jsx(CurrentPlan, { customerId, onChangePlan }) }),
2417
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Usage", children: /* @__PURE__ */ jsxRuntime.jsx(UsageDashboard, { features: usageFeatures, loading: usageLoading }) }),
2418
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Invoices", children: /* @__PURE__ */ jsxRuntime.jsx(InvoiceList, { customerId }) }),
2419
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Credits", children: /* @__PURE__ */ jsxRuntime.jsx(CreditBalance, { customerId }) })
782
2420
  ]
783
2421
  }
784
2422
  );
785
2423
  }
786
- var fontFamily3 = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
787
- var primaryColor = "#6366f1";
788
- var primaryHover = "#4f46e5";
789
- function UpgradePrompt({
790
- feature,
791
- variant = "card",
792
- upgradeUrl = "/upgrade",
793
- message = "Upgrade your plan to unlock this feature",
794
- className,
795
- style,
796
- ...props
2424
+ var BRAND_LABELS = {
2425
+ visa: "Visa",
2426
+ mastercard: "Mastercard",
2427
+ amex: "American Express",
2428
+ discover: "Discover",
2429
+ jcb: "JCB",
2430
+ diners: "Diners Club",
2431
+ unionpay: "UnionPay"
2432
+ };
2433
+ function PaymentMethodDisplay({
2434
+ paymentMethod,
2435
+ onUpdatePaymentMethod,
2436
+ updateHref = "/billing/update-payment"
797
2437
  }) {
798
- const { allowed, isLoading } = useCan(feature ?? "");
799
- if (isLoading || allowed || !feature) return null;
800
- if (variant === "inline") {
801
- const inlineStyle = {
802
- fontFamily: fontFamily3,
803
- fontSize: 13,
804
- color: "#6b7280",
805
- ...style
806
- };
807
- const linkStyle = {
808
- color: primaryColor,
809
- fontWeight: 600,
810
- textDecoration: "none",
811
- marginLeft: 4
812
- };
813
- return /* @__PURE__ */ jsxRuntime.jsxs(
814
- "span",
2438
+ const handleUpdate = () => {
2439
+ if (onUpdatePaymentMethod) {
2440
+ onUpdatePaymentMethod();
2441
+ } else {
2442
+ window.location.href = updateHref;
2443
+ }
2444
+ };
2445
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2446
+ paymentMethod ? /* @__PURE__ */ jsxRuntime.jsxs(
2447
+ "div",
815
2448
  {
816
- "data-nozle": "upgrade-prompt",
817
- "data-variant": "inline",
818
- className,
819
- style: inlineStyle,
820
- ...props,
2449
+ style: {
2450
+ display: "flex",
2451
+ alignItems: "center",
2452
+ gap: "0.75rem",
2453
+ padding: "0.75rem 1rem",
2454
+ border: "1px solid var(--nozle-border, var(--border))",
2455
+ borderRadius: "0.5rem",
2456
+ fontSize: "0.875rem"
2457
+ },
821
2458
  children: [
822
- message,
823
2459
  /* @__PURE__ */ jsxRuntime.jsx(
824
- "a",
2460
+ "span",
825
2461
  {
826
- href: upgradeUrl,
827
- "data-nozle": "upgrade-prompt-link",
828
- style: linkStyle,
829
- onMouseEnter: (e) => {
830
- e.currentTarget.style.textDecoration = "underline";
831
- },
832
- onMouseLeave: (e) => {
833
- e.currentTarget.style.textDecoration = "none";
2462
+ style: {
2463
+ fontWeight: 600,
2464
+ color: "var(--nozle-foreground, var(--foreground))"
834
2465
  },
835
- children: "Upgrade"
2466
+ children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand
836
2467
  }
837
- )
838
- ]
839
- }
840
- );
841
- }
842
- if (variant === "banner") {
843
- const bannerStyle = {
844
- fontFamily: fontFamily3,
845
- display: "flex",
846
- alignItems: "center",
847
- justifyContent: "space-between",
848
- gap: 16,
849
- background: "rgba(99, 102, 241, 0.06)",
850
- border: "1px solid rgba(99, 102, 241, 0.15)",
851
- borderRadius: 10,
852
- padding: "12px 20px",
853
- ...style
854
- };
855
- const msgStyle2 = {
856
- fontSize: 14,
857
- fontWeight: 500,
858
- color: "#374151",
859
- flex: 1
860
- };
861
- const btnStyle2 = {
862
- display: "inline-flex",
863
- alignItems: "center",
864
- justifyContent: "center",
865
- fontSize: 13,
866
- fontWeight: 600,
867
- color: "#ffffff",
868
- background: primaryColor,
869
- border: "none",
870
- borderRadius: 8,
871
- padding: "8px 18px",
872
- textDecoration: "none",
873
- cursor: "pointer",
874
- whiteSpace: "nowrap",
875
- transition: "background 0.15s ease"
876
- };
877
- return /* @__PURE__ */ jsxRuntime.jsxs(
878
- "div",
879
- {
880
- "data-nozle": "upgrade-prompt",
881
- "data-variant": "banner",
882
- className,
883
- style: bannerStyle,
884
- ...props,
885
- children: [
886
- /* @__PURE__ */ jsxRuntime.jsx("span", { "data-nozle": "upgrade-prompt-message", style: msgStyle2, children: message }),
887
- /* @__PURE__ */ jsxRuntime.jsx(
888
- "a",
2468
+ ),
2469
+ /* @__PURE__ */ jsxRuntime.jsxs(
2470
+ "span",
889
2471
  {
890
- href: upgradeUrl,
891
- "data-nozle": "upgrade-prompt-action",
892
- style: btnStyle2,
893
- onMouseEnter: (e) => {
894
- e.currentTarget.style.background = primaryHover;
2472
+ style: {
2473
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
895
2474
  },
896
- onMouseLeave: (e) => {
897
- e.currentTarget.style.background = primaryColor;
2475
+ children: [
2476
+ "\u2022\u2022\u2022\u2022 ",
2477
+ paymentMethod.last4
2478
+ ]
2479
+ }
2480
+ ),
2481
+ /* @__PURE__ */ jsxRuntime.jsxs(
2482
+ "span",
2483
+ {
2484
+ style: {
2485
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2486
+ fontSize: "0.75rem"
898
2487
  },
899
- children: "Upgrade"
2488
+ children: [
2489
+ "Expires ",
2490
+ String(paymentMethod.expMonth).padStart(2, "0"),
2491
+ "/",
2492
+ String(paymentMethod.expYear).slice(-2)
2493
+ ]
900
2494
  }
901
2495
  )
902
2496
  ]
903
2497
  }
904
- );
905
- }
906
- const cardStyle = {
907
- fontFamily: fontFamily3,
908
- maxWidth: 400,
909
- background: "#ffffff",
910
- border: "1px solid #e5e7eb",
911
- borderRadius: 16,
912
- padding: 24,
913
- boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
914
- textAlign: "center",
915
- ...style
916
- };
917
- const iconWrapStyle = {
918
- display: "inline-flex",
919
- alignItems: "center",
920
- justifyContent: "center",
921
- width: 48,
922
- height: 48,
923
- borderRadius: 12,
924
- background: "rgba(99, 102, 241, 0.08)",
925
- marginBottom: 16
2498
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2499
+ "p",
2500
+ {
2501
+ style: {
2502
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2503
+ fontSize: "0.875rem"
2504
+ },
2505
+ children: "No payment method on file."
2506
+ }
2507
+ ),
2508
+ /* @__PURE__ */ jsxRuntime.jsx(
2509
+ "button",
2510
+ {
2511
+ onClick: handleUpdate,
2512
+ style: {
2513
+ background: "transparent",
2514
+ border: "1px solid var(--nozle-primary, var(--primary))",
2515
+ color: "var(--nozle-primary, var(--primary))",
2516
+ padding: "0.375rem 0.875rem",
2517
+ borderRadius: "0.375rem",
2518
+ fontSize: "0.875rem",
2519
+ fontWeight: 500,
2520
+ cursor: "pointer"
2521
+ },
2522
+ children: "Update Payment Method"
2523
+ }
2524
+ )
2525
+ ] });
2526
+ }
2527
+ var CANCEL_REASONS = [
2528
+ "Too expensive",
2529
+ "Missing features",
2530
+ "Switching to competitor",
2531
+ "Not using it enough",
2532
+ "Other"
2533
+ ];
2534
+ function CancelSubscriptionButton({
2535
+ subscriptionId,
2536
+ onCancelled
2537
+ }) {
2538
+ const [step, setStep] = React.useState("idle");
2539
+ const [selectedReason, setSelectedReason] = React.useState("");
2540
+ const [otherText, setOtherText] = React.useState("");
2541
+ const [loading, setLoading] = React.useState(false);
2542
+ const handleDismiss = () => {
2543
+ setStep("idle");
2544
+ setSelectedReason("");
2545
+ setOtherText("");
926
2546
  };
927
- const msgStyle = {
928
- fontSize: 15,
929
- fontWeight: 500,
930
- color: "#374151",
931
- lineHeight: 1.5,
932
- margin: 0,
933
- marginBottom: 20
2547
+ const handleConfirmCancel = () => {
2548
+ setStep("reason");
934
2549
  };
935
- const btnStyle = {
936
- display: "inline-flex",
937
- alignItems: "center",
938
- justifyContent: "center",
939
- fontSize: 14,
940
- fontWeight: 600,
941
- color: "#ffffff",
942
- background: primaryColor,
943
- border: "none",
944
- borderRadius: 10,
945
- padding: "10px 24px",
946
- textDecoration: "none",
947
- cursor: "pointer",
948
- transition: "background 0.15s ease",
949
- width: "100%"
2550
+ const handleSubmitCancellation = async () => {
2551
+ const reason = selectedReason === "Other" ? otherText : selectedReason;
2552
+ setLoading(true);
2553
+ await fetch(`/api/v1/subscriptions/${subscriptionId}`, {
2554
+ method: "DELETE",
2555
+ headers: { "Content-Type": "application/json" },
2556
+ body: JSON.stringify({ reason })
2557
+ });
2558
+ setLoading(false);
2559
+ handleDismiss();
2560
+ onCancelled?.();
950
2561
  };
951
- return /* @__PURE__ */ jsxRuntime.jsxs(
952
- "div",
953
- {
954
- "data-nozle": "upgrade-prompt",
955
- "data-variant": "card",
956
- className,
957
- style: cardStyle,
958
- ...props,
959
- children: [
960
- /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-nozle": "upgrade-prompt-content", children: [
961
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: iconWrapStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
962
- "svg",
963
- {
964
- width: "24",
965
- height: "24",
966
- viewBox: "0 0 24 24",
967
- fill: "none",
968
- stroke: primaryColor,
969
- strokeWidth: "2",
970
- strokeLinecap: "round",
971
- strokeLinejoin: "round",
972
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" })
973
- }
974
- ) }),
975
- /* @__PURE__ */ jsxRuntime.jsx("p", { "data-nozle": "upgrade-prompt-message", style: msgStyle, children: message })
976
- ] }),
977
- /* @__PURE__ */ jsxRuntime.jsx(
978
- "a",
2562
+ const isSubmitDisabled = loading || !selectedReason || selectedReason === "Other" && !otherText.trim();
2563
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2564
+ /* @__PURE__ */ jsxRuntime.jsx(
2565
+ "button",
2566
+ {
2567
+ onClick: () => setStep("confirm"),
2568
+ style: {
2569
+ background: "transparent",
2570
+ border: "1px solid var(--nozle-destructive, var(--destructive, #dc2626))",
2571
+ color: "var(--nozle-destructive, var(--destructive, #dc2626))",
2572
+ padding: "0.375rem 0.875rem",
2573
+ borderRadius: "0.375rem",
2574
+ fontSize: "0.875rem",
2575
+ fontWeight: 500,
2576
+ cursor: "pointer"
2577
+ },
2578
+ children: "Cancel Subscription"
2579
+ }
2580
+ ),
2581
+ step === "confirm" && /* @__PURE__ */ jsxRuntime.jsx(
2582
+ "div",
2583
+ {
2584
+ role: "dialog",
2585
+ "aria-modal": "true",
2586
+ "aria-label": "Cancel subscription confirmation",
2587
+ style: {
2588
+ position: "fixed",
2589
+ inset: 0,
2590
+ display: "flex",
2591
+ alignItems: "center",
2592
+ justifyContent: "center",
2593
+ background: "rgba(0,0,0,0.4)",
2594
+ zIndex: 50
2595
+ },
2596
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2597
+ "div",
979
2598
  {
980
- href: upgradeUrl,
981
- "data-nozle": "upgrade-prompt-action",
982
- style: btnStyle,
983
- onMouseEnter: (e) => {
984
- e.currentTarget.style.background = primaryHover;
2599
+ style: {
2600
+ background: "var(--nozle-background, var(--background, #fff))",
2601
+ border: "1px solid var(--nozle-border, var(--border))",
2602
+ borderRadius: "0.75rem",
2603
+ padding: "1.5rem",
2604
+ maxWidth: "28rem",
2605
+ width: "90%",
2606
+ boxShadow: "0 20px 60px rgba(0,0,0,0.15)"
985
2607
  },
986
- onMouseLeave: (e) => {
987
- e.currentTarget.style.background = primaryColor;
2608
+ children: [
2609
+ /* @__PURE__ */ jsxRuntime.jsx(
2610
+ "h2",
2611
+ {
2612
+ style: {
2613
+ fontSize: "1.125rem",
2614
+ fontWeight: 600,
2615
+ marginBottom: "0.75rem"
2616
+ },
2617
+ children: "Cancel Subscription?"
2618
+ }
2619
+ ),
2620
+ /* @__PURE__ */ jsxRuntime.jsx(
2621
+ "p",
2622
+ {
2623
+ style: {
2624
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2625
+ fontSize: "0.875rem",
2626
+ marginBottom: "1.5rem"
2627
+ },
2628
+ children: "Your access will end at the end of the billing period."
2629
+ }
2630
+ ),
2631
+ /* @__PURE__ */ jsxRuntime.jsxs(
2632
+ "div",
2633
+ {
2634
+ style: {
2635
+ display: "flex",
2636
+ gap: "0.75rem",
2637
+ justifyContent: "flex-end"
2638
+ },
2639
+ children: [
2640
+ /* @__PURE__ */ jsxRuntime.jsx(
2641
+ "button",
2642
+ {
2643
+ onClick: handleDismiss,
2644
+ style: {
2645
+ background: "transparent",
2646
+ border: "1px solid var(--nozle-border, var(--border))",
2647
+ color: "var(--nozle-foreground, var(--foreground))",
2648
+ padding: "0.375rem 0.875rem",
2649
+ borderRadius: "0.375rem",
2650
+ fontSize: "0.875rem",
2651
+ cursor: "pointer"
2652
+ },
2653
+ children: "Keep Subscription"
2654
+ }
2655
+ ),
2656
+ /* @__PURE__ */ jsxRuntime.jsx(
2657
+ "button",
2658
+ {
2659
+ onClick: handleConfirmCancel,
2660
+ style: {
2661
+ background: "var(--nozle-destructive, var(--destructive, #dc2626))",
2662
+ border: "1px solid transparent",
2663
+ color: "#fff",
2664
+ padding: "0.375rem 0.875rem",
2665
+ borderRadius: "0.375rem",
2666
+ fontSize: "0.875rem",
2667
+ fontWeight: 500,
2668
+ cursor: "pointer"
2669
+ },
2670
+ children: "Confirm Cancel"
2671
+ }
2672
+ )
2673
+ ]
2674
+ }
2675
+ )
2676
+ ]
2677
+ }
2678
+ )
2679
+ }
2680
+ ),
2681
+ step === "reason" && /* @__PURE__ */ jsxRuntime.jsx(
2682
+ "div",
2683
+ {
2684
+ role: "dialog",
2685
+ "aria-modal": "true",
2686
+ "aria-label": "Cancellation reason survey",
2687
+ style: {
2688
+ position: "fixed",
2689
+ inset: 0,
2690
+ display: "flex",
2691
+ alignItems: "center",
2692
+ justifyContent: "center",
2693
+ background: "rgba(0,0,0,0.4)",
2694
+ zIndex: 50
2695
+ },
2696
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2697
+ "div",
2698
+ {
2699
+ style: {
2700
+ background: "var(--nozle-background, var(--background, #fff))",
2701
+ border: "1px solid var(--nozle-border, var(--border))",
2702
+ borderRadius: "0.75rem",
2703
+ padding: "1.5rem",
2704
+ maxWidth: "28rem",
2705
+ width: "90%",
2706
+ boxShadow: "0 20px 60px rgba(0,0,0,0.15)"
988
2707
  },
989
- children: "Upgrade Now"
2708
+ children: [
2709
+ /* @__PURE__ */ jsxRuntime.jsx(
2710
+ "h2",
2711
+ {
2712
+ style: {
2713
+ fontSize: "1.125rem",
2714
+ fontWeight: 600,
2715
+ marginBottom: "0.5rem"
2716
+ },
2717
+ children: "Why are you cancelling?"
2718
+ }
2719
+ ),
2720
+ /* @__PURE__ */ jsxRuntime.jsx(
2721
+ "p",
2722
+ {
2723
+ style: {
2724
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2725
+ fontSize: "0.875rem",
2726
+ marginBottom: "1rem"
2727
+ },
2728
+ children: "Your feedback helps us improve."
2729
+ }
2730
+ ),
2731
+ /* @__PURE__ */ jsxRuntime.jsxs(
2732
+ "fieldset",
2733
+ {
2734
+ style: {
2735
+ border: "none",
2736
+ padding: 0,
2737
+ margin: 0,
2738
+ marginBottom: "1rem"
2739
+ },
2740
+ children: [
2741
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { className: "sr-only", children: "Cancellation reason" }),
2742
+ CANCEL_REASONS.map((r) => /* @__PURE__ */ jsxRuntime.jsxs(
2743
+ "label",
2744
+ {
2745
+ style: {
2746
+ display: "flex",
2747
+ alignItems: "center",
2748
+ gap: "0.5rem",
2749
+ padding: "0.375rem 0",
2750
+ fontSize: "0.875rem",
2751
+ cursor: "pointer"
2752
+ },
2753
+ children: [
2754
+ /* @__PURE__ */ jsxRuntime.jsx(
2755
+ "input",
2756
+ {
2757
+ type: "radio",
2758
+ name: "cancel-reason",
2759
+ value: r,
2760
+ checked: selectedReason === r,
2761
+ onChange: () => setSelectedReason(r),
2762
+ "aria-label": r
2763
+ }
2764
+ ),
2765
+ r
2766
+ ]
2767
+ },
2768
+ r
2769
+ ))
2770
+ ]
2771
+ }
2772
+ ),
2773
+ selectedReason === "Other" && /* @__PURE__ */ jsxRuntime.jsx(
2774
+ "input",
2775
+ {
2776
+ type: "text",
2777
+ placeholder: "Please describe your reason",
2778
+ value: otherText,
2779
+ onChange: (e) => setOtherText(e.target.value),
2780
+ "aria-label": "Other reason",
2781
+ style: {
2782
+ width: "100%",
2783
+ border: "1px solid var(--nozle-border, var(--border))",
2784
+ borderRadius: "0.375rem",
2785
+ padding: "0.5rem 0.75rem",
2786
+ fontSize: "0.875rem",
2787
+ marginBottom: "1rem",
2788
+ background: "var(--nozle-background, var(--background, #fff))",
2789
+ color: "var(--nozle-foreground, var(--foreground))",
2790
+ boxSizing: "border-box"
2791
+ }
2792
+ }
2793
+ ),
2794
+ /* @__PURE__ */ jsxRuntime.jsxs(
2795
+ "div",
2796
+ {
2797
+ style: {
2798
+ display: "flex",
2799
+ gap: "0.75rem",
2800
+ justifyContent: "flex-end"
2801
+ },
2802
+ children: [
2803
+ /* @__PURE__ */ jsxRuntime.jsx(
2804
+ "button",
2805
+ {
2806
+ onClick: handleDismiss,
2807
+ style: {
2808
+ background: "transparent",
2809
+ border: "1px solid var(--nozle-border, var(--border))",
2810
+ color: "var(--nozle-foreground, var(--foreground))",
2811
+ padding: "0.375rem 0.875rem",
2812
+ borderRadius: "0.375rem",
2813
+ fontSize: "0.875rem",
2814
+ cursor: "pointer"
2815
+ },
2816
+ children: "Keep Subscription"
2817
+ }
2818
+ ),
2819
+ /* @__PURE__ */ jsxRuntime.jsx(
2820
+ "button",
2821
+ {
2822
+ onClick: handleSubmitCancellation,
2823
+ disabled: isSubmitDisabled,
2824
+ style: {
2825
+ background: isSubmitDisabled ? "var(--nozle-muted, var(--muted, #f1f5f9))" : "var(--nozle-destructive, var(--destructive, #dc2626))",
2826
+ border: "1px solid transparent",
2827
+ color: isSubmitDisabled ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "#fff",
2828
+ padding: "0.375rem 0.875rem",
2829
+ borderRadius: "0.375rem",
2830
+ fontSize: "0.875rem",
2831
+ fontWeight: 500,
2832
+ cursor: isSubmitDisabled ? "not-allowed" : "pointer"
2833
+ },
2834
+ children: loading ? "Cancelling..." : "Submit Cancellation"
2835
+ }
2836
+ )
2837
+ ]
2838
+ }
2839
+ )
2840
+ ]
990
2841
  }
991
2842
  )
992
- ]
993
- }
994
- );
2843
+ }
2844
+ )
2845
+ ] });
995
2846
  }
996
2847
 
2848
+ exports.BillingContext = BillingContext;
2849
+ exports.BillingPortal = BillingPortal;
2850
+ exports.BillingPortalProvider = BillingPortalProvider;
997
2851
  exports.BillingProvider = BillingProvider;
2852
+ exports.CANCEL_REASONS = CANCEL_REASONS;
2853
+ exports.CancelSubscriptionButton = CancelSubscriptionButton;
2854
+ exports.Checkout = Checkout;
2855
+ exports.CheckoutButton = CheckoutButton;
2856
+ exports.CreditBalance = CreditBalance;
2857
+ exports.CreditHistory = CreditHistory;
2858
+ exports.CreditTopUpButton = CreditTopUpButton;
2859
+ exports.CurrentPlan = CurrentPlan;
2860
+ exports.FeatureGate = FeatureGate;
2861
+ exports.InvoiceList = InvoiceList;
2862
+ exports.LockedOverlay = LockedOverlay;
2863
+ exports.PaymentMethodDisplay = PaymentMethodDisplay;
998
2864
  exports.PlanBadge = PlanBadge;
2865
+ exports.PlanCard = PlanCard;
2866
+ exports.PlanComparison = PlanComparison;
2867
+ exports.PlanGate = PlanGate;
2868
+ exports.PricingTable = PricingTable;
2869
+ exports.UpgradeButton = UpgradeButton;
2870
+ exports.UpgradeModal = UpgradeModal;
999
2871
  exports.UpgradePrompt = UpgradePrompt;
2872
+ exports.UsageAlert = UsageAlert;
2873
+ exports.UsageDashboard = UsageDashboard;
2874
+ exports.UsageGate = UsageGate;
1000
2875
  exports.UsageMeter = UsageMeter;
2876
+ exports.getUsageMeterColor = getUsageMeterColor;
1001
2877
  exports.useBillingContext = useBillingContext;
2878
+ exports.useBillingPortal = useBillingPortal;
1002
2879
  exports.useCan = useCan;
1003
2880
  exports.useCheckout = useCheckout;
1004
- exports.usePlan = usePlan;
1005
- exports.usePlans = usePlans;
1006
- exports.useSubscribe = useSubscribe;
2881
+ exports.useCredits = useCredits;
2882
+ exports.useNozleClient = useNozleClient;
1007
2883
  exports.useUsage = useUsage;
1008
2884
  //# sourceMappingURL=index.cjs.map
1009
2885
  //# sourceMappingURL=index.cjs.map