@nozle-js/react 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1002 +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) {
98
- const errorBody = await res.json().catch(() => ({}));
99
- throw new Error(errorBody.error || "Failed to create checkout session");
100
- }
101
- return res.json();
102
- }
103
- async fetchPlans() {
104
- const res = await fetch(`${this.baseUrl}/v1/plans`, {
105
- 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
+ }
106
153
  });
107
- if (!res.ok) return [];
108
- const data = await res.json();
109
- return data.plans || [];
110
- }
111
- handleEvent(event) {
112
- if (event.type === "usage.updated") {
113
- const metric = event.metric;
114
- const used = event.used;
115
- const limit = event.limit;
116
- const remaining = event.remaining;
117
- this.updateUsage(metric, used, limit, remaining);
118
- }
119
- }
120
- };
121
- function connectCentrifugo(store, customerId, wsUrl) {
122
- const centrifuge = new Centrifuge(wsUrl, {});
123
- const sub = centrifuge.newSubscription(
124
- `customer:${customerId}`
125
- );
126
- sub.on("publication", (ctx) => {
127
- store.handleEvent(
128
- ctx.data
129
- );
130
- });
131
- sub.on("subscribed", () => {
132
- store.setConnectionState("connected");
133
- });
134
- centrifuge.on("connected", () => {
135
- store.setConnectionState("connected");
136
- });
137
- centrifuge.on("disconnected", () => {
138
- store.setConnectionState("disconnected");
139
- });
140
- sub.subscribe();
141
- centrifuge.connect();
142
- store.setConnectionState("connecting");
143
- return {
144
- disconnect: () => {
154
+ sub.subscribe();
155
+ cf.connect();
156
+ return () => {
145
157
  sub.unsubscribe();
146
- 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;
147
174
  }
148
- };
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;
149
219
  }
150
- var BillingContext = createContext(null);
151
- function BillingProvider({
152
- apiKey,
153
- customerId,
154
- baseUrl = "http://localhost:8080",
155
- wsUrl = "ws://localhost:8001/connection/websocket",
156
- stripeKey,
157
- features = [],
158
- children
159
- }) {
160
- const storeRef = useRef(null);
161
- if (!storeRef.current) {
162
- storeRef.current = new BillingStore(baseUrl, apiKey, customerId);
163
- }
164
- const stripePromise = useMemo(
165
- () => stripeKey ? loadStripe(stripeKey) : null,
166
- [stripeKey]
167
- );
220
+ function useCredits(customerId) {
221
+ const { client } = useBillingContext();
222
+ const [state, setState] = useState({
223
+ balance: null,
224
+ loading: true,
225
+ error: null
226
+ });
168
227
  useEffect(() => {
169
- const store = storeRef.current;
170
- for (const feature of features) {
171
- store.fetchUsage(feature);
172
- }
173
- const connection = connectCentrifugo(
174
- store,
175
- customerId,
176
- wsUrl
177
- );
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();
178
275
  return () => {
179
- connection.disconnect();
276
+ cancelled = true;
180
277
  };
181
- }, [apiKey, customerId, baseUrl, wsUrl, features]);
182
- return /* @__PURE__ */ jsx(BillingContext.Provider, { value: { store: storeRef.current, stripePromise }, children });
278
+ }, [client, customerId]);
279
+ return state;
183
280
  }
184
- function useBillingContext() {
185
- const ctx = useContext(BillingContext);
281
+ var BillingPortalContext = createContext(
282
+ null
283
+ );
284
+ function useBillingPortal() {
285
+ const ctx = useContext(BillingPortalContext);
186
286
  if (!ctx) {
187
- throw new Error("useBillingContext must be used within a BillingProvider");
287
+ throw new Error(
288
+ "useBillingPortal must be used inside BillingPortalProvider"
289
+ );
188
290
  }
189
291
  return ctx;
190
292
  }
191
- function useCan(feature) {
192
- const { store } = useBillingContext();
193
- const state = useSyncExternalStore(
194
- store.subscribe,
195
- store.getSnapshot,
196
- store.getServerSnapshot
197
- );
198
- if (state.error) {
199
- return { allowed: false, isLoading: false, error: state.error };
200
- }
201
- if (state.entitlements === null) {
202
- return { allowed: false, isLoading: true, error: null };
203
- }
204
- const featureState = state.entitlements.features[feature];
205
- return {
206
- allowed: featureState?.enabled ?? false,
207
- isLoading: false,
208
- error: null
209
- };
293
+ function BillingPortalProvider({
294
+ customerId,
295
+ apiKey,
296
+ children
297
+ }) {
298
+ return /* @__PURE__ */ jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
210
299
  }
211
- function useUsage(metric) {
212
- const { store } = useBillingContext();
213
- const state = useSyncExternalStore(
214
- store.subscribe,
215
- store.getSnapshot,
216
- 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
+ }
217
442
  );
218
- if (state.error) {
219
- return { data: null, isLoading: false, error: state.error };
220
- }
221
- if (state.entitlements === null) {
222
- return { data: null, isLoading: true, error: null };
223
- }
224
- const usage = state.usage[metric] ?? null;
225
- return { data: usage, isLoading: false, error: null };
226
443
  }
227
- function usePlan() {
228
- const { store } = useBillingContext();
229
- const state = useSyncExternalStore(
230
- store.subscribe,
231
- store.getSnapshot,
232
- 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
+ }
233
527
  );
234
- if (state.error) {
235
- return { data: null, isLoading: false, error: state.error };
236
- }
237
- if (state.entitlements === null) {
238
- return { data: null, isLoading: true, error: null };
239
- }
240
- return {
241
- data: {
242
- plan_slug: state.entitlements.plan_slug,
243
- subscription_status: state.entitlements.subscription_status
244
- },
245
- isLoading: false,
246
- error: null
247
- };
248
528
  }
249
- function useCheckout() {
250
- const { store, stripePromise } = useBillingContext();
251
- const [isLoading, setIsLoading] = useState(false);
252
- const [error, setError] = useState(null);
253
- const [checkout, setCheckout] = useState(null);
254
- const fetchClientSecret = useCallback(
255
- async (planCode, successUrl) => {
256
- setIsLoading(true);
257
- setError(null);
258
- try {
259
- const result = await store.fetchCheckoutSecret(planCode, successUrl);
260
- setCheckout(result);
261
- return result.client_secret;
262
- } catch (err) {
263
- const e = err instanceof Error ? err : new Error(String(err));
264
- setError(e);
265
- throw e;
266
- } finally {
267
- setIsLoading(false);
268
- }
269
- },
270
- [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
+ }
271
629
  );
272
- return { fetchClientSecret, checkout, stripePromise, isLoading, error };
273
630
  }
274
- function useSubscribe() {
275
- const { store } = useBillingContext();
276
- const [isLoading, setIsLoading] = useState(false);
277
- const [error, setError] = useState(null);
278
- const subscribe = useCallback(
279
- async (planCode) => {
280
- setIsLoading(true);
281
- setError(null);
282
- try {
283
- const res = await fetch(`${store.baseUrl}/v1/subscribe`, {
284
- method: "POST",
285
- headers: {
286
- Authorization: `Bearer ${store.apiKey}`,
287
- "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);
288
686
  },
289
- body: JSON.stringify({
290
- plan_code: planCode,
291
- customer_id: store.customerId
292
- })
687
+ modal: {
688
+ ondismiss: () => setLoading(false)
689
+ }
293
690
  });
294
- if (!res.ok) throw new Error("Failed to create subscription");
295
- return await res.json();
296
- } catch (err) {
297
- const e = err instanceof Error ? err : new Error(String(err));
298
- setError(e);
299
- throw e;
300
- } finally {
301
- 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"));
302
706
  }
303
- },
304
- [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
+ }
305
734
  );
306
- return { subscribe, isLoading, error };
307
735
  }
308
- function usePlans() {
309
- const { store } = useBillingContext();
310
- const [plans, setPlans] = useState([]);
311
- const [isLoading, setIsLoading] = useState(true);
312
- useEffect(() => {
313
- store.fetchPlans().then(setPlans).finally(() => setIsLoading(false));
314
- }, [store]);
315
- return { plans, isLoading };
316
- }
317
- var fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
318
- var levelColors = {
319
- normal: {
320
- fg: "#6366f1",
321
- bg: "rgba(99, 102, 241, 0.08)",
322
- trackBg: "#e5e7eb"
323
- },
324
- warning: {
325
- fg: "#f59e0b",
326
- bg: "rgba(245, 158, 11, 0.08)",
327
- trackBg: "#e5e7eb"
328
- },
329
- critical: {
330
- fg: "#ef4444",
331
- bg: "rgba(239, 68, 68, 0.08)",
332
- 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");
333
741
  }
334
- };
335
- var skeletonStyleInjected = false;
336
- function ensureKeyframes() {
337
- if (skeletonStyleInjected || typeof document === "undefined") return;
338
- const sheet = document.createElement("style");
339
- sheet.textContent = `@keyframes nozle-pulse{0%,100%{opacity:1}50%{opacity:.4}}`;
340
- document.head.appendChild(sheet);
341
- skeletonStyleInjected = true;
742
+ return ctx;
342
743
  }
343
- function UsageMeter({
344
- metric,
345
- 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,
346
767
  className,
347
- style,
348
- ...props
768
+ style
349
769
  }) {
350
- const { data, isLoading, error } = useUsage(metric);
351
- if (isLoading) {
352
- ensureKeyframes();
353
- if (variant === "minimal") {
354
- return /* @__PURE__ */ jsx(
355
- "span",
356
- {
357
- "data-nozle": "usage-meter",
358
- "data-variant": "minimal",
359
- className,
360
- style: {
361
- display: "inline-block",
362
- width: 80,
363
- height: 16,
364
- borderRadius: 4,
365
- background: "#e5e7eb",
366
- animation: "nozle-pulse 1.5s ease-in-out infinite",
367
- ...style
368
- },
369
- ...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"
805
+ }
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 })
370
942
  }
371
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;
372
2194
  }
373
- if (variant === "ring") {
374
- return /* @__PURE__ */ jsx(
375
- "div",
376
- {
377
- "data-nozle": "usage-meter",
378
- "data-variant": "ring",
379
- className,
380
- style: {
381
- width: 96,
382
- height: 96,
383
- borderRadius: "50%",
384
- background: "#e5e7eb",
385
- animation: "nozle-pulse 1.5s ease-in-out infinite",
386
- ...style
387
- },
388
- ...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}`);
389
2214
  }
390
- );
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
+ }
391
2226
  }
392
- 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(
393
2234
  "div",
394
2235
  {
395
- "data-nozle": "usage-meter",
396
- "data-variant": "bar",
397
- 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
+ {
398
2256
  style: {
399
- maxWidth: 360,
400
- fontFamily,
401
- ...style
2257
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2258
+ fontSize: "0.875rem",
2259
+ textAlign: "center",
2260
+ padding: "1.5rem"
402
2261
  },
403
- ...props,
404
- children: /* @__PURE__ */ jsx(
405
- "div",
406
- {
407
- style: {
408
- height: 8,
409
- borderRadius: 4,
410
- background: "#e5e7eb",
411
- animation: "nozle-pulse 1.5s ease-in-out infinite"
412
- }
413
- }
414
- )
2262
+ children: "No invoices yet."
415
2263
  }
416
2264
  );
417
2265
  }
418
- if (error || !data) return null;
419
- const percent = data.limit > 0 ? Math.round(data.used / data.limit * 100) : 0;
420
- const level = percent >= 90 ? "critical" : percent >= 75 ? "warning" : "normal";
421
- const colors = levelColors[level];
422
- if (variant === "minimal") {
423
- const minimalStyle = {
424
- fontFamily,
425
- fontSize: 13,
426
- fontWeight: 500,
427
- color: colors.fg,
428
- background: colors.bg,
429
- padding: "3px 8px",
430
- borderRadius: 6,
431
- whiteSpace: "nowrap",
432
- ...style
433
- };
434
- return /* @__PURE__ */ jsxs(
435
- "span",
2266
+ return /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
2267
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs(
2268
+ "tr",
436
2269
  {
437
- "data-nozle": "usage-meter",
438
- "data-variant": "minimal",
439
- "data-level": level,
440
- className,
441
- style: minimalStyle,
442
- ...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
+ },
443
2275
  children: [
444
- data.used.toLocaleString(),
445
- "/",
446
- 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" })
447
2281
  ]
448
2282
  }
449
- );
450
- }
451
- if (variant === "ring") {
452
- const size = 96;
453
- const strokeWidth = 7;
454
- const radius = (size - strokeWidth) / 2;
455
- const circumference = 2 * Math.PI * radius;
456
- const offset = circumference - percent / 100 * circumference;
457
- const containerStyle = {
458
- position: "relative",
459
- display: "inline-flex",
460
- alignItems: "center",
461
- justifyContent: "center",
462
- width: size,
463
- height: size,
464
- fontFamily,
465
- ...style
466
- };
467
- const labelStyle2 = {
468
- position: "absolute",
469
- inset: 0,
470
- display: "flex",
471
- flexDirection: "column",
472
- alignItems: "center",
473
- justifyContent: "center",
474
- fontWeight: 700,
475
- fontSize: 20,
476
- lineHeight: 1,
477
- color: colors.fg
478
- };
479
- return /* @__PURE__ */ jsxs(
480
- "div",
2283
+ ) }),
2284
+ /* @__PURE__ */ jsx("tbody", { children: invoices.map((inv) => /* @__PURE__ */ jsxs(
2285
+ "tr",
481
2286
  {
482
- "data-nozle": "usage-meter",
483
- "data-variant": "ring",
484
- "data-level": level,
485
- className,
486
- style: containerStyle,
487
- ...props,
2287
+ style: {
2288
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
2289
+ fontSize: "0.875rem"
2290
+ },
488
2291
  children: [
489
- /* @__PURE__ */ jsxs(
490
- "svg",
2292
+ /* @__PURE__ */ jsx(
2293
+ "td",
491
2294
  {
492
- viewBox: `0 0 ${size} ${size}`,
493
- width: size,
494
- height: size,
495
- style: { display: "block", transform: "rotate(-90deg)" },
496
- children: [
497
- /* @__PURE__ */ jsx(
498
- "circle",
499
- {
500
- cx: size / 2,
501
- cy: size / 2,
502
- r: radius,
503
- fill: "none",
504
- stroke: colors.trackBg,
505
- strokeWidth
506
- }
507
- ),
508
- /* @__PURE__ */ jsx(
509
- "circle",
510
- {
511
- cx: size / 2,
512
- cy: size / 2,
513
- r: radius,
514
- fill: "none",
515
- stroke: colors.fg,
516
- strokeWidth,
517
- strokeDasharray: circumference,
518
- strokeDashoffset: offset,
519
- strokeLinecap: "round",
520
- style: { transition: "stroke-dashoffset 0.4s ease" }
521
- }
522
- )
523
- ]
2295
+ style: {
2296
+ padding: "0.5rem",
2297
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2298
+ },
2299
+ children: inv.number
524
2300
  }
525
2301
  ),
526
- /* @__PURE__ */ jsxs("span", { "data-nozle": "usage-meter-label", style: labelStyle2, children: [
527
- /* @__PURE__ */ jsxs("span", { children: [
528
- percent,
529
- "%"
530
- ] }),
531
- /* @__PURE__ */ jsx(
532
- "span",
533
- {
534
- style: {
535
- fontSize: 10,
536
- fontWeight: 400,
537
- color: "#6b7280",
538
- marginTop: 2
539
- },
540
- children: "used"
541
- }
542
- )
543
- ] })
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
+ ) })
544
2354
  ]
545
- }
546
- );
547
- }
548
- const cardStyle = {
549
- fontFamily,
550
- maxWidth: 360,
551
- background: "#ffffff",
552
- border: "1px solid #e5e7eb",
553
- borderRadius: 12,
554
- padding: "16px 20px",
555
- boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
556
- ...style
557
- };
558
- const headerStyle = {
559
- display: "flex",
560
- justifyContent: "space-between",
561
- alignItems: "baseline",
562
- marginBottom: 10
563
- };
564
- const labelStyle = {
565
- fontSize: 13,
566
- fontWeight: 500,
567
- color: "#374151"
568
- };
569
- const valueStyle = {
570
- fontSize: 13,
571
- fontWeight: 600,
572
- color: colors.fg
573
- };
574
- const trackStyle = {
575
- height: 8,
576
- borderRadius: 4,
577
- background: colors.trackBg,
578
- overflow: "hidden"
579
- };
580
- const fillStyle = {
581
- height: "100%",
582
- borderRadius: 4,
583
- background: colors.fg,
584
- width: `${Math.min(percent, 100)}%`,
585
- transition: "width 0.4s ease"
586
- };
2355
+ },
2356
+ inv.id
2357
+ )) })
2358
+ ] });
2359
+ }
2360
+ function Section({
2361
+ title,
2362
+ children
2363
+ }) {
587
2364
  return /* @__PURE__ */ jsxs(
588
2365
  "div",
589
2366
  {
590
- "data-nozle": "usage-meter",
591
- "data-variant": "bar",
592
- "data-level": level,
593
- className,
594
- style: cardStyle,
595
- ...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
+ },
596
2373
  children: [
597
- /* @__PURE__ */ jsxs("div", { style: headerStyle, children: [
598
- /* @__PURE__ */ jsxs("span", { "data-nozle": "usage-meter-label", style: labelStyle, children: [
599
- data.used.toLocaleString(),
600
- " / ",
601
- data.limit.toLocaleString()
602
- ] }),
603
- /* @__PURE__ */ jsxs("span", { style: valueStyle, children: [
604
- percent,
605
- "%"
606
- ] })
607
- ] }),
608
- /* @__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
609
2389
  ]
610
2390
  }
611
2391
  );
612
2392
  }
613
- var fontFamily2 = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
614
- var statusColorMap = {
615
- active: { dot: "#22c55e", bg: "rgba(34, 197, 94, 0.08)", text: "#15803d" },
616
- trialing: { dot: "#6366f1", bg: "rgba(99, 102, 241, 0.08)", text: "#4f46e5" },
617
- past_due: { dot: "#f59e0b", bg: "rgba(245, 158, 11, 0.08)", text: "#b45309" },
618
- canceled: { dot: "#6b7280", bg: "rgba(107, 114, 128, 0.08)", text: "#4b5563" },
619
- unpaid: { dot: "#ef4444", bg: "rgba(239, 68, 68, 0.08)", text: "#dc2626" },
620
- incomplete: { dot: "#f59e0b", bg: "rgba(245, 158, 11, 0.08)", text: "#b45309" }
621
- };
622
- var defaultStatusColors = { dot: "#6366f1", bg: "rgba(99, 102, 241, 0.08)", text: "#4f46e5" };
623
- function getStatusColors(status) {
624
- return statusColorMap[status] ?? defaultStatusColors;
625
- }
626
- var skeletonStyleInjected2 = false;
627
- function ensureKeyframes2() {
628
- if (skeletonStyleInjected2 || typeof document === "undefined") return;
629
- const sheet = document.createElement("style");
630
- sheet.textContent = `@keyframes nozle-pulse{0%,100%{opacity:1}50%{opacity:.4}}`;
631
- document.head.appendChild(sheet);
632
- skeletonStyleInjected2 = true;
633
- }
634
- function PlanBadge({
635
- variant = "pill",
636
- className,
637
- style,
638
- ...props
2393
+ function BillingPortal({
2394
+ customerId,
2395
+ usageFeatures,
2396
+ usageLoading = false,
2397
+ onChangePlan
639
2398
  }) {
640
- const { data, isLoading, error } = usePlan();
641
- if (isLoading) {
642
- ensureKeyframes2();
643
- if (variant === "icon") {
644
- return /* @__PURE__ */ jsx(
645
- "span",
646
- {
647
- "data-nozle": "plan-badge",
648
- "data-variant": "icon",
649
- className,
650
- style: {
651
- display: "inline-block",
652
- width: 32,
653
- height: 32,
654
- borderRadius: "50%",
655
- background: "#e5e7eb",
656
- animation: "nozle-pulse 1.5s ease-in-out infinite",
657
- ...style
658
- },
659
- ...props
660
- }
661
- );
662
- }
663
- return /* @__PURE__ */ jsx(
664
- "span",
665
- {
666
- "data-nozle": "plan-badge",
667
- "data-variant": variant,
668
- className,
669
- style: {
670
- display: "inline-block",
671
- width: 72,
672
- height: 22,
673
- borderRadius: variant === "pill" ? 999 : 4,
674
- background: "#e5e7eb",
675
- animation: "nozle-pulse 1.5s ease-in-out infinite",
676
- ...style
677
- },
678
- ...props
679
- }
680
- );
681
- }
682
- if (error || !data) return null;
683
- const statusLabel = data.subscription_status.replace(/_/g, " ");
684
- const colors = getStatusColors(data.subscription_status);
685
- const planName = data.plan_slug;
686
- if (variant === "text") {
687
- const textStyle = {
688
- fontFamily: fontFamily2,
689
- display: "inline-flex",
690
- alignItems: "center",
691
- gap: 6,
692
- fontSize: 14,
693
- fontWeight: 500,
694
- color: "#111827",
695
- ...style
696
- };
697
- const dotStyle2 = {
698
- width: 8,
699
- height: 8,
700
- borderRadius: "50%",
701
- background: colors.dot,
702
- flexShrink: 0
703
- };
704
- return /* @__PURE__ */ jsxs(
705
- "span",
706
- {
707
- "data-nozle": "plan-badge",
708
- "data-variant": "text",
709
- "data-status": data.subscription_status,
710
- className,
711
- style: textStyle,
712
- ...props,
713
- children: [
714
- /* @__PURE__ */ jsx("span", { style: dotStyle2, "aria-hidden": "true" }),
715
- planName
716
- ]
717
- }
718
- );
719
- }
720
- if (variant === "icon") {
721
- const iconStyle = {
722
- fontFamily: fontFamily2,
723
- display: "inline-flex",
724
- alignItems: "center",
725
- justifyContent: "center",
726
- width: 32,
727
- height: 32,
728
- borderRadius: "50%",
729
- background: colors.bg,
730
- color: colors.text,
731
- fontSize: 14,
732
- fontWeight: 700,
733
- lineHeight: 1,
734
- flexShrink: 0,
735
- ...style
736
- };
737
- return /* @__PURE__ */ jsx(
738
- "span",
739
- {
740
- "data-nozle": "plan-badge",
741
- "data-variant": "icon",
742
- "data-status": data.subscription_status,
743
- className,
744
- style: iconStyle,
745
- title: `${planName} (${statusLabel})`,
746
- ...props,
747
- children: planName.charAt(0).toUpperCase()
748
- }
749
- );
750
- }
751
- const pillStyle = {
752
- fontFamily: fontFamily2,
753
- display: "inline-flex",
754
- alignItems: "center",
755
- gap: 6,
756
- fontSize: 12,
757
- fontWeight: 600,
758
- lineHeight: 1.4,
759
- color: colors.text,
760
- background: colors.bg,
761
- padding: "4px 12px",
762
- borderRadius: 999,
763
- whiteSpace: "nowrap",
764
- ...style
765
- };
766
- const dotStyle = {
767
- width: 6,
768
- height: 6,
769
- borderRadius: "50%",
770
- background: colors.dot,
771
- flexShrink: 0
772
- };
773
2399
  return /* @__PURE__ */ jsxs(
774
- "span",
2400
+ "div",
775
2401
  {
776
- "data-nozle": "plan-badge",
777
- "data-variant": "pill",
778
- "data-status": data.subscription_status,
779
- className,
780
- style: pillStyle,
781
- ...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
+ },
782
2409
  children: [
783
- /* @__PURE__ */ jsx("span", { style: dotStyle, "aria-hidden": "true" }),
784
- 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 }) })
785
2414
  ]
786
2415
  }
787
2416
  );
788
2417
  }
789
- var fontFamily3 = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
790
- var primaryColor = "#6366f1";
791
- var primaryHover = "#4f46e5";
792
- function UpgradePrompt({
793
- feature,
794
- variant = "card",
795
- upgradeUrl = "/upgrade",
796
- message = "Upgrade your plan to unlock this feature",
797
- className,
798
- style,
799
- ...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"
800
2431
  }) {
801
- const { allowed, isLoading } = useCan(feature ?? "");
802
- if (isLoading || allowed || !feature) return null;
803
- if (variant === "inline") {
804
- const inlineStyle = {
805
- fontFamily: fontFamily3,
806
- fontSize: 13,
807
- color: "#6b7280",
808
- ...style
809
- };
810
- const linkStyle = {
811
- color: primaryColor,
812
- fontWeight: 600,
813
- textDecoration: "none",
814
- marginLeft: 4
815
- };
816
- return /* @__PURE__ */ jsxs(
817
- "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",
818
2442
  {
819
- "data-nozle": "upgrade-prompt",
820
- "data-variant": "inline",
821
- className,
822
- style: inlineStyle,
823
- ...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
+ },
824
2452
  children: [
825
- message,
826
2453
  /* @__PURE__ */ jsx(
827
- "a",
2454
+ "span",
828
2455
  {
829
- href: upgradeUrl,
830
- "data-nozle": "upgrade-prompt-link",
831
- style: linkStyle,
832
- onMouseEnter: (e) => {
833
- e.currentTarget.style.textDecoration = "underline";
834
- },
835
- onMouseLeave: (e) => {
836
- e.currentTarget.style.textDecoration = "none";
2456
+ style: {
2457
+ fontWeight: 600,
2458
+ color: "var(--nozle-foreground, var(--foreground))"
837
2459
  },
838
- children: "Upgrade"
2460
+ children: BRAND_LABELS[paymentMethod.brand.toLowerCase()] ?? paymentMethod.brand
839
2461
  }
840
- )
841
- ]
842
- }
843
- );
844
- }
845
- if (variant === "banner") {
846
- const bannerStyle = {
847
- fontFamily: fontFamily3,
848
- display: "flex",
849
- alignItems: "center",
850
- justifyContent: "space-between",
851
- gap: 16,
852
- background: "rgba(99, 102, 241, 0.06)",
853
- border: "1px solid rgba(99, 102, 241, 0.15)",
854
- borderRadius: 10,
855
- padding: "12px 20px",
856
- ...style
857
- };
858
- const msgStyle2 = {
859
- fontSize: 14,
860
- fontWeight: 500,
861
- color: "#374151",
862
- flex: 1
863
- };
864
- const btnStyle2 = {
865
- display: "inline-flex",
866
- alignItems: "center",
867
- justifyContent: "center",
868
- fontSize: 13,
869
- fontWeight: 600,
870
- color: "#ffffff",
871
- background: primaryColor,
872
- border: "none",
873
- borderRadius: 8,
874
- padding: "8px 18px",
875
- textDecoration: "none",
876
- cursor: "pointer",
877
- whiteSpace: "nowrap",
878
- transition: "background 0.15s ease"
879
- };
880
- return /* @__PURE__ */ jsxs(
881
- "div",
882
- {
883
- "data-nozle": "upgrade-prompt",
884
- "data-variant": "banner",
885
- className,
886
- style: bannerStyle,
887
- ...props,
888
- children: [
889
- /* @__PURE__ */ jsx("span", { "data-nozle": "upgrade-prompt-message", style: msgStyle2, children: message }),
890
- /* @__PURE__ */ jsx(
891
- "a",
2462
+ ),
2463
+ /* @__PURE__ */ jsxs(
2464
+ "span",
892
2465
  {
893
- href: upgradeUrl,
894
- "data-nozle": "upgrade-prompt-action",
895
- style: btnStyle2,
896
- onMouseEnter: (e) => {
897
- e.currentTarget.style.background = primaryHover;
2466
+ style: {
2467
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
898
2468
  },
899
- onMouseLeave: (e) => {
900
- 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"
901
2481
  },
902
- children: "Upgrade"
2482
+ children: [
2483
+ "Expires ",
2484
+ String(paymentMethod.expMonth).padStart(2, "0"),
2485
+ "/",
2486
+ String(paymentMethod.expYear).slice(-2)
2487
+ ]
903
2488
  }
904
2489
  )
905
2490
  ]
906
2491
  }
907
- );
908
- }
909
- const cardStyle = {
910
- fontFamily: fontFamily3,
911
- maxWidth: 400,
912
- background: "#ffffff",
913
- border: "1px solid #e5e7eb",
914
- borderRadius: 16,
915
- padding: 24,
916
- boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
917
- textAlign: "center",
918
- ...style
919
- };
920
- const iconWrapStyle = {
921
- display: "inline-flex",
922
- alignItems: "center",
923
- justifyContent: "center",
924
- width: 48,
925
- height: 48,
926
- borderRadius: 12,
927
- background: "rgba(99, 102, 241, 0.08)",
928
- 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("");
929
2540
  };
930
- const msgStyle = {
931
- fontSize: 15,
932
- fontWeight: 500,
933
- color: "#374151",
934
- lineHeight: 1.5,
935
- margin: 0,
936
- marginBottom: 20
2541
+ const handleConfirmCancel = () => {
2542
+ setStep("reason");
937
2543
  };
938
- const btnStyle = {
939
- display: "inline-flex",
940
- alignItems: "center",
941
- justifyContent: "center",
942
- fontSize: 14,
943
- fontWeight: 600,
944
- color: "#ffffff",
945
- background: primaryColor,
946
- border: "none",
947
- borderRadius: 10,
948
- padding: "10px 24px",
949
- textDecoration: "none",
950
- cursor: "pointer",
951
- transition: "background 0.15s ease",
952
- 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?.();
953
2555
  };
954
- return /* @__PURE__ */ jsxs(
955
- "div",
956
- {
957
- "data-nozle": "upgrade-prompt",
958
- "data-variant": "card",
959
- className,
960
- style: cardStyle,
961
- ...props,
962
- children: [
963
- /* @__PURE__ */ jsxs("div", { "data-nozle": "upgrade-prompt-content", children: [
964
- /* @__PURE__ */ jsx("div", { style: iconWrapStyle, children: /* @__PURE__ */ jsx(
965
- "svg",
966
- {
967
- width: "24",
968
- height: "24",
969
- viewBox: "0 0 24 24",
970
- fill: "none",
971
- stroke: primaryColor,
972
- strokeWidth: "2",
973
- strokeLinecap: "round",
974
- strokeLinejoin: "round",
975
- children: /* @__PURE__ */ jsx("path", { d: "M13 2L3 14h9l-1 8 10-12h-9l1-8z" })
976
- }
977
- ) }),
978
- /* @__PURE__ */ jsx("p", { "data-nozle": "upgrade-prompt-message", style: msgStyle, children: message })
979
- ] }),
980
- /* @__PURE__ */ jsx(
981
- "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",
982
2592
  {
983
- href: upgradeUrl,
984
- "data-nozle": "upgrade-prompt-action",
985
- style: btnStyle,
986
- onMouseEnter: (e) => {
987
- 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)"
988
2601
  },
989
- onMouseLeave: (e) => {
990
- 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)"
991
2701
  },
992
- 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
+ ]
993
2835
  }
994
2836
  )
995
- ]
996
- }
997
- );
2837
+ }
2838
+ )
2839
+ ] });
998
2840
  }
999
2841
 
1000
- 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 };
1001
2843
  //# sourceMappingURL=index.js.map
1002
2844
  //# sourceMappingURL=index.js.map