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