@nozle-js/react 0.5.3 → 0.7.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,301 +1,247 @@
1
- import React, { createContext, useMemo, useState, useEffect, useContext } from 'react';
2
- import { Centrifuge } from 'centrifuge';
3
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
1
+ import React, { createContext, useMemo, useContext, useState, useEffect, useCallback } from 'react';
2
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
+ import { EmbeddedCheckoutProvider, EmbeddedCheckout, Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
5
4
  import { loadStripe } from '@stripe/stripe-js';
6
5
 
7
6
  // src/provider.tsx
8
- function createClient(apiKey, baseUrl) {
7
+ function createClient(publishableKey, baseUrl) {
9
8
  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
9
  return {
21
- apiKey,
10
+ publishableKey,
22
11
  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();
12
+ catalogFetch(path, init) {
13
+ return fetch(`${base}${path}`, {
14
+ ...init,
15
+ headers: {
16
+ Authorization: `Bearer ${publishableKey}`,
17
+ "Content-Type": "application/json",
18
+ ...init?.headers
19
+ }
20
+ });
28
21
  }
29
22
  };
30
23
  }
31
- var BillingContext = createContext({
32
- client: null,
33
- workspaceId: "",
34
- centrifugoUrl: "",
35
- centrifugoToken: null
36
- });
24
+ var BillingContext = createContext(null);
37
25
  function BillingProvider({
38
- apiKey,
26
+ publishableKey,
27
+ createCheckout,
39
28
  baseUrl = "https://api.nozle.app",
40
- workspaceId = "",
41
- centrifugoUrl,
42
29
  children
43
30
  }) {
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();
61
- return () => {
62
- cancelled = true;
63
- };
64
- }, [client]);
65
- const contextValue = {
66
- client,
67
- workspaceId,
68
- centrifugoUrl: resolvedCentrifugoUrl,
69
- centrifugoToken
70
- };
31
+ if (!publishableKey.startsWith("pk_")) {
32
+ throw new Error("BillingProvider publishableKey must be a publishable key (pk_)");
33
+ }
34
+ const client = useMemo(
35
+ () => createClient(publishableKey, baseUrl),
36
+ [publishableKey, baseUrl]
37
+ );
38
+ const contextValue = useMemo(
39
+ () => ({ client, createCheckout }),
40
+ [client, createCheckout]
41
+ );
71
42
  return React.createElement(BillingContext.Provider, { value: contextValue }, children);
72
43
  }
73
- function useBillingContext() {
44
+ function useOptionalBillingContext() {
74
45
  return useContext(BillingContext);
75
46
  }
47
+ function useBillingContext() {
48
+ const context = useOptionalBillingContext();
49
+ if (!context) {
50
+ throw new Error("useBillingContext must be used within a BillingProvider");
51
+ }
52
+ return context;
53
+ }
76
54
  function useNozleClient() {
77
- const { client } = useBillingContext();
78
- if (!client) throw new Error("useNozleClient must be used within <BillingProvider>");
79
- return client;
55
+ return useBillingContext().client;
80
56
  }
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);
57
+ function usePlans() {
58
+ const { client } = useBillingContext();
59
+ const [plans, setPlans] = useState([]);
60
+ const [isLoading, setIsLoading] = useState(true);
91
61
  useEffect(() => {
92
- if (!client) {
93
- setState({
94
- ...initialState,
95
- loading: false,
96
- error: "No client in context"
97
- });
98
- return;
99
- }
100
62
  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
- }
125
- }
126
- }
127
- void checkEntitlement();
128
- return () => {
129
- cancelled = true;
130
- };
131
- }, [client, customerId, featureKey]);
132
- useEffect(() => {
133
- if (!centrifugoToken || !centrifugoUrl || !workspaceId) {
63
+ if (!client) {
64
+ setIsLoading(false);
134
65
  return;
135
66
  }
136
- const cf = new Centrifuge(centrifugoUrl, {
137
- token: centrifugoToken
138
- });
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
- }
67
+ void client.catalogFetch("/api/v1/plans").then(async (response) => {
68
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
69
+ const data = await response.json();
70
+ if (!cancelled) setPlans(data.plans ?? []);
71
+ }).finally(() => {
72
+ if (!cancelled) setIsLoading(false);
153
73
  });
154
- sub.subscribe();
155
- cf.connect();
156
74
  return () => {
157
- sub.unsubscribe();
158
- cf.disconnect();
75
+ cancelled = true;
159
76
  };
160
- }, [centrifugoToken, centrifugoUrl, workspaceId, customerId, featureKey]);
161
- return state;
77
+ }, [client]);
78
+ return { plans, isLoading };
162
79
  }
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;
174
- }
175
- let cancelled = false;
176
- async function fetchUsage() {
80
+
81
+ // src/components/billing/checkout-navigation.ts
82
+ function navigateToCheckout(url, location = window.location) {
83
+ location.assign(url);
84
+ }
85
+
86
+ // src/hooks/use-checkout.ts
87
+ function useCheckout() {
88
+ const { createCheckout } = useBillingContext();
89
+ const [isLoading, setIsLoading] = useState(false);
90
+ const [error, setError] = useState(null);
91
+ const [checkout, setCheckout] = useState(null);
92
+ const fetchClientSecret = useCallback(
93
+ async (planCode, returnUrl) => {
94
+ setIsLoading(true);
95
+ setError(null);
177
96
  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}`);
97
+ if (!createCheckout) {
98
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
197
99
  }
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
- });
100
+ const result = await createCheckout({
101
+ planCode,
102
+ returnUrl: returnUrl ?? window.location.href
103
+ });
104
+ setCheckout(result);
105
+ if ("client_secret" in result || "clientSecret" in result) {
106
+ const clientSecret = result.client_secret ?? result.clientSecret;
107
+ if (clientSecret) return clientSecret;
108
+ }
109
+ if ("url" in result && result.url) {
110
+ navigateToCheckout(result.url);
111
+ return null;
205
112
  }
206
- } catch (err) {
207
- if (!cancelled) {
208
- const message = err instanceof Error ? err.message : "Unknown error";
209
- setState({ value: null, loading: false, error: message });
113
+ if ("type" in result && (result.type === "completed" || result.type === "scheduled")) {
114
+ return null;
210
115
  }
116
+ throw new Error("Checkout response did not include a supported result type");
117
+ } catch (cause) {
118
+ const checkoutError = cause instanceof Error ? cause : new Error(String(cause));
119
+ setError(checkoutError);
120
+ throw checkoutError;
121
+ } finally {
122
+ setIsLoading(false);
211
123
  }
212
- }
213
- void fetchUsage();
214
- return () => {
215
- cancelled = true;
216
- };
217
- }, [client, customerId, metricKey]);
218
- return state;
124
+ },
125
+ [createCheckout]
126
+ );
127
+ return { fetchClientSecret, checkout, isLoading, error };
219
128
  }
220
- function useCredits(customerId) {
221
- const { client } = useBillingContext();
222
- const [state, setState] = useState({
223
- balance: null,
224
- loading: true,
225
- error: null
226
- });
227
- useEffect(() => {
228
- if (!client) {
229
- setState({
230
- balance: null,
231
- loading: false,
232
- error: "No client in context"
233
- });
129
+ function loadRazorpayScript() {
130
+ return new Promise((resolve, reject) => {
131
+ if (document.getElementById("razorpay-js")) {
132
+ resolve();
234
133
  return;
235
134
  }
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();
275
- return () => {
276
- cancelled = true;
277
- };
278
- }, [client, customerId]);
279
- return state;
135
+ const script = document.createElement("script");
136
+ script.id = "razorpay-js";
137
+ script.src = "https://checkout.razorpay.com/v1/checkout.js";
138
+ script.onload = () => resolve();
139
+ script.onerror = () => reject(new Error("Failed to load Razorpay.js"));
140
+ document.body.appendChild(script);
141
+ });
280
142
  }
281
- var BillingPortalContext = createContext(
282
- null
283
- );
284
- function useBillingPortal() {
285
- const ctx = useContext(BillingPortalContext);
286
- if (!ctx) {
287
- throw new Error(
288
- "useBillingPortal must be used inside BillingPortalProvider"
289
- );
143
+ async function handleCheckoutResult(result, options) {
144
+ if ("type" in result && result.type === "scheduled") {
145
+ options.onScheduled?.(result);
146
+ return;
290
147
  }
291
- return ctx;
148
+ if ("type" in result && result.type === "completed") {
149
+ options.onComplete?.(result);
150
+ return;
151
+ }
152
+ if ("type" in result && result.type === "razorpay") {
153
+ if (!options.razorpayKeyId) throw new Error("razorpayKeyId is required for Razorpay checkout");
154
+ await loadRazorpayScript();
155
+ const checkout = new window.Razorpay({
156
+ key: options.razorpayKeyId,
157
+ order_id: result.orderId,
158
+ handler: (response) => options.onSuccess?.(response.razorpay_payment_id)
159
+ });
160
+ checkout.open();
161
+ return;
162
+ }
163
+ if ("url" in result && result.url) {
164
+ window.location.assign(result.url);
165
+ return;
166
+ }
167
+ if ("type" in result && result.type === "stripe") {
168
+ const clientSecret = result.clientSecret ?? result.client_secret;
169
+ if (clientSecret && options.onStripeClientSecret) {
170
+ options.onStripeClientSecret(clientSecret);
171
+ return;
172
+ }
173
+ if (clientSecret) throw new Error("onStripeClientSecret is required for embedded Stripe checkout");
174
+ throw new Error("Stripe checkout did not include a URL or client secret");
175
+ }
176
+ throw new Error("Unknown checkout response format");
292
177
  }
293
- function BillingPortalProvider({
294
- customerId,
295
- apiKey,
296
- children
178
+ function CheckoutButton({
179
+ planCode,
180
+ returnUrl,
181
+ label = "Get Started",
182
+ className,
183
+ style,
184
+ onError,
185
+ razorpayKeyId,
186
+ onStripeClientSecret,
187
+ onSuccess,
188
+ onComplete,
189
+ onScheduled
297
190
  }) {
298
- return /* @__PURE__ */ jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
191
+ const { createCheckout } = useBillingContext();
192
+ const [loading, setLoading] = useState(false);
193
+ const [error, setError] = useState(null);
194
+ async function handleClick() {
195
+ setLoading(true);
196
+ setError(null);
197
+ try {
198
+ if (!createCheckout) {
199
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
200
+ }
201
+ const result = await createCheckout({
202
+ planCode,
203
+ returnUrl: returnUrl ?? window.location.href
204
+ });
205
+ await handleCheckoutResult(result, {
206
+ razorpayKeyId,
207
+ onStripeClientSecret,
208
+ onSuccess,
209
+ onComplete,
210
+ onScheduled
211
+ });
212
+ } catch (cause) {
213
+ const checkoutError = cause instanceof Error ? cause : new Error("Checkout failed");
214
+ setError(checkoutError);
215
+ onError?.(checkoutError);
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ }
220
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
221
+ /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ onClick: () => void handleClick(),
226
+ disabled: loading,
227
+ className,
228
+ style: {
229
+ padding: "0.75rem 1.5rem",
230
+ borderRadius: "var(--nozle-radius, 0.5rem)",
231
+ border: "none",
232
+ background: "var(--nozle-primary, var(--primary))",
233
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
234
+ cursor: loading ? "not-allowed" : "pointer",
235
+ fontWeight: 500,
236
+ opacity: loading ? 0.7 : 1,
237
+ ...style
238
+ },
239
+ "aria-busy": loading,
240
+ children: loading ? "Loading..." : label
241
+ }
242
+ ),
243
+ error && /* @__PURE__ */ jsx("span", { role: "alert", children: error.message })
244
+ ] });
299
245
  }
300
246
  var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
301
247
  function fmtPrice(cents, cur) {
@@ -547,28 +493,38 @@ function Skeleton() {
547
493
  ] }, i)) });
548
494
  }
549
495
  function PricingTable({
550
- customerId,
551
496
  currentPlanCode,
552
497
  plans: propPlans,
553
498
  features,
554
499
  onSelect,
500
+ returnUrl,
501
+ onStripeClientSecret,
502
+ onCheckoutResult,
503
+ onCheckoutError,
555
504
  showToggle = true,
556
505
  highlightPlan,
557
506
  className,
558
507
  enterpriseEmail
559
508
  }) {
560
- const client = useNozleClient();
509
+ const billing = useOptionalBillingContext();
510
+ const client = billing?.client;
561
511
  const [plans, setPlans] = useState(propPlans ?? []);
562
512
  const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
563
513
  const [isAnnual, setIsAnnual] = useState(false);
564
514
  const [loading, setLoading] = useState(!propPlans);
515
+ const [checkoutError, setCheckoutError] = useState(null);
565
516
  useEffect(() => {
566
517
  if (propPlans) {
567
518
  setPlans(propPlans);
568
519
  return;
569
520
  }
521
+ if (!client) {
522
+ setLoading(false);
523
+ setCheckoutError("BillingProvider with a publishableKey is required to load plans");
524
+ return;
525
+ }
570
526
  let cancelled = false;
571
- client.fetch("/api/v1/plans").then(async (res) => {
527
+ client.catalogFetch("/api/v1/plans").then(async (res) => {
572
528
  if (!cancelled && res.ok) {
573
529
  const data = await res.json();
574
530
  setPlans(data.plans || []);
@@ -581,23 +537,30 @@ function PricingTable({
581
537
  };
582
538
  }, [client, propPlans]);
583
539
  useEffect(() => {
584
- if (currentPlanCode !== void 0) {
585
- setCurrentPlan(currentPlanCode);
540
+ if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
541
+ }, [currentPlanCode]);
542
+ async function selectPlan(plan) {
543
+ setCheckoutError(null);
544
+ if (onSelect) {
545
+ onSelect(plan);
586
546
  return;
587
547
  }
588
- if (!customerId) return;
589
- let cancelled = false;
590
- client.fetch(`/api/v1/billing/status?customer_id=${encodeURIComponent(customerId)}`).then(async (res) => {
591
- if (!cancelled && res.ok) {
592
- const data = await res.json();
593
- setCurrentPlan(data.plan ?? data.planName ?? null);
548
+ try {
549
+ if (!billing?.createCheckout) {
550
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
594
551
  }
595
- }).catch(() => {
596
- });
597
- return () => {
598
- cancelled = true;
599
- };
600
- }, [client, customerId, currentPlanCode]);
552
+ const result = await billing.createCheckout({
553
+ planCode: plan.code,
554
+ returnUrl: returnUrl ?? window.location.href
555
+ });
556
+ onCheckoutResult?.(result);
557
+ await handleCheckoutResult(result, { onStripeClientSecret });
558
+ } catch (cause) {
559
+ const error = cause instanceof Error ? cause : new Error("Checkout failed");
560
+ setCheckoutError(error.message);
561
+ onCheckoutError?.(error);
562
+ }
563
+ }
601
564
  const hasAnnual = plans.some((p) => p.interval === "yearly");
602
565
  const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
603
566
  if (loading) return /* @__PURE__ */ jsx(Skeleton, {});
@@ -607,6 +570,7 @@ function PricingTable({
607
570
  background: C.bg
608
571
  }, children: [
609
572
  showToggle && hasAnnual && /* @__PURE__ */ jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
573
+ checkoutError && /* @__PURE__ */ jsx("p", { role: "alert", children: checkoutError }),
610
574
  /* @__PURE__ */ jsx("div", { style: {
611
575
  display: "grid",
612
576
  gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
@@ -620,7 +584,7 @@ function PricingTable({
620
584
  isHighlighted: highlightPlan === plan.code,
621
585
  isEnt: isEnterprise(plan),
622
586
  enterpriseEmail,
623
- onSelect: onSelect ? () => onSelect(plan) : void 0
587
+ onSelect: () => void selectPlan(plan)
624
588
  },
625
589
  plan.code
626
590
  )) })
@@ -635,6 +599,7 @@ function PlanCard({
635
599
  description,
636
600
  isAnnual,
637
601
  isCurrent,
602
+ returnUrl,
638
603
  onSelect
639
604
  }) {
640
605
  const price = isAnnual ? annualPrice : monthlyPrice;
@@ -750,7 +715,10 @@ function PlanCard({
750
715
  onSelect && /* @__PURE__ */ jsx(
751
716
  "button",
752
717
  {
753
- onClick: onSelect,
718
+ onClick: () => onSelect?.({
719
+ planCode: id,
720
+ returnUrl: returnUrl ?? window.location.href
721
+ }),
754
722
  style: {
755
723
  background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
756
724
  color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
@@ -872,113 +840,8 @@ function PlanComparison({
872
840
  }
873
841
  );
874
842
  }
875
- function loadRazorpayScript() {
876
- return new Promise((resolve, reject) => {
877
- if (document.getElementById("razorpay-js")) {
878
- resolve();
879
- return;
880
- }
881
- const script = document.createElement("script");
882
- script.id = "razorpay-js";
883
- script.src = "https://checkout.razorpay.com/v1/checkout.js";
884
- script.onload = () => resolve();
885
- script.onerror = () => reject(new Error("Failed to load Razorpay.js"));
886
- document.body.appendChild(script);
887
- });
888
- }
889
- function CheckoutButton({
890
- planId,
891
- label = "Get Started",
892
- apiBaseUrl = "https://api.nozle.app",
893
- className,
894
- style,
895
- onError,
896
- razorpayKeyId,
897
- onStripeClientSecret,
898
- onSuccess
899
- }) {
900
- const { customerId, apiKey } = useBillingPortal();
901
- const [loading, setLoading] = useState(false);
902
- async function handleClick() {
903
- setLoading(true);
904
- try {
905
- const response = await fetch(`${apiBaseUrl}/api/v1/checkout`, {
906
- method: "POST",
907
- headers: {
908
- "Content-Type": "application/json",
909
- Authorization: `Bearer ${apiKey}`
910
- },
911
- body: JSON.stringify({ planId, customerId })
912
- });
913
- if (!response.ok) {
914
- throw new Error(`HTTP ${response.status}: Checkout failed`);
915
- }
916
- const data = await response.json();
917
- if ("type" in data && data.type === "razorpay") {
918
- if (!razorpayKeyId) {
919
- onError?.(
920
- new Error("razorpayKeyId prop required for Razorpay checkout")
921
- );
922
- return;
923
- }
924
- await loadRazorpayScript();
925
- const rzp = new window.Razorpay({
926
- key: razorpayKeyId,
927
- order_id: data.orderId,
928
- handler: (r) => {
929
- onSuccess?.(r.razorpay_payment_id);
930
- },
931
- modal: {
932
- ondismiss: () => setLoading(false)
933
- }
934
- });
935
- rzp.open();
936
- } else if ("type" in data && data.type === "stripe") {
937
- if (data.url) {
938
- window.location.href = data.url;
939
- } else if (data.clientSecret) {
940
- onStripeClientSecret?.(data.clientSecret);
941
- } else {
942
- onError?.(
943
- new Error("Stripe checkout: no url or clientSecret in response")
944
- );
945
- }
946
- } else if ("url" in data && data.url) {
947
- window.location.href = data.url;
948
- } else {
949
- onError?.(new Error("Unknown checkout response format"));
950
- }
951
- } catch (err) {
952
- const error = err instanceof Error ? err : new Error("Checkout failed");
953
- onError?.(error);
954
- } finally {
955
- setLoading(false);
956
- }
957
- }
958
- return /* @__PURE__ */ jsx(
959
- "button",
960
- {
961
- onClick: () => void handleClick(),
962
- disabled: loading,
963
- className,
964
- style: {
965
- padding: "0.75rem 1.5rem",
966
- borderRadius: "var(--nozle-radius, 0.5rem)",
967
- border: "none",
968
- background: "var(--nozle-primary, var(--primary))",
969
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
970
- cursor: loading ? "not-allowed" : "pointer",
971
- fontWeight: 500,
972
- opacity: loading ? 0.7 : 1,
973
- ...style
974
- },
975
- "aria-busy": loading,
976
- children: loading ? "Loading..." : label
977
- }
978
- );
979
- }
980
843
  var CheckoutContext = createContext(null);
981
- function useCheckout() {
844
+ function useCheckout2() {
982
845
  const ctx = useContext(CheckoutContext);
983
846
  if (ctx === null) {
984
847
  throw new Error("useCheckout must be used inside a <Checkout> component");
@@ -1009,7 +872,9 @@ function CheckoutInner({
1009
872
  onError,
1010
873
  onReady,
1011
874
  className,
1012
- style
875
+ style,
876
+ returnUrl,
877
+ children
1013
878
  }) {
1014
879
  const stripe = useStripe();
1015
880
  const elements = useElements();
@@ -1022,7 +887,7 @@ function CheckoutInner({
1022
887
  setError(null);
1023
888
  const result = await stripe.confirmPayment({
1024
889
  elements,
1025
- confirmParams: { return_url: window.location.href },
890
+ confirmParams: { return_url: returnUrl ?? window.location.href },
1026
891
  redirect: "if_required"
1027
892
  });
1028
893
  setIsProcessing(false);
@@ -1059,7 +924,7 @@ function CheckoutInner({
1059
924
  }
1060
925
  ),
1061
926
  error !== null && /* @__PURE__ */ jsx("p", { style: { color: "var(--nozle-destructive, #dc2626)", margin: "0.5rem 0 0" }, children: error.message }),
1062
- /* @__PURE__ */ jsx(
927
+ children ?? /* @__PURE__ */ jsx(
1063
928
  "button",
1064
929
  {
1065
930
  type: "button",
@@ -1087,18 +952,28 @@ function Checkout({
1087
952
  clientSecret,
1088
953
  publishableKey,
1089
954
  stripeAccount,
955
+ returnUrl,
1090
956
  submitLabel,
1091
957
  onSuccess,
958
+ onComplete,
1092
959
  onError,
1093
960
  onReady,
1094
961
  className,
1095
- style
962
+ style,
963
+ children
1096
964
  }) {
1097
965
  const stripePromise = useMemo(
1098
966
  () => loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1099
967
  [publishableKey, stripeAccount]
1100
968
  );
1101
969
  const appearance = useMemo(() => buildStripeAppearance(), []);
970
+ const embeddedOptions = useMemo(
971
+ () => ({ clientSecret, onComplete }),
972
+ [clientSecret, onComplete]
973
+ );
974
+ if (clientSecret.startsWith("cs_")) {
975
+ return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(EmbeddedCheckoutProvider, { stripe: stripePromise, options: embeddedOptions, children: /* @__PURE__ */ jsx(EmbeddedCheckout, {}) }) });
976
+ }
1102
977
  const options = { clientSecret, appearance };
1103
978
  return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
1104
979
  CheckoutInner,
@@ -1108,278 +983,151 @@ function Checkout({
1108
983
  onError,
1109
984
  onReady,
1110
985
  className,
1111
- style
986
+ style,
987
+ returnUrl,
988
+ children
1112
989
  }
1113
990
  ) }, clientSecret);
1114
991
  }
1115
992
  function UpgradeModal({
1116
993
  isOpen,
1117
- targetPlanId,
1118
- customerId,
1119
- apiBaseUrl = "https://api.nozle.app",
1120
- apiKey = "",
994
+ planCode,
995
+ returnUrl,
996
+ preview,
997
+ onStripeClientSecret,
998
+ onCheckoutStarted,
999
+ onCompleted,
1000
+ onScheduled,
1121
1001
  onConfirm,
1122
- onCancel
1002
+ onCancel,
1003
+ onError
1123
1004
  }) {
1124
- const [preview, setPreview] = useState(null);
1125
- const [loading, setLoading] = useState(false);
1005
+ const { createCheckout } = useBillingContext();
1126
1006
  const [error, setError] = useState(null);
1127
1007
  const [confirming, setConfirming] = useState(false);
1128
- useEffect(() => {
1129
- if (!isOpen || !targetPlanId || !customerId) {
1130
- return;
1131
- }
1132
- let cancelled = false;
1133
- setLoading(true);
1134
- setError(null);
1135
- setPreview(null);
1136
- async function fetchPreview() {
1137
- try {
1138
- const response = await fetch(
1139
- `${apiBaseUrl}/api/v1/subscriptions/preview`,
1140
- {
1141
- method: "POST",
1142
- headers: {
1143
- "Content-Type": "application/json",
1144
- Authorization: `Bearer ${apiKey}`
1145
- },
1146
- body: JSON.stringify({ targetPlanId, customerId })
1147
- }
1148
- );
1149
- if (!response.ok) {
1150
- throw new Error(
1151
- `HTTP ${response.status}: Failed to load proration preview`
1152
- );
1153
- }
1154
- const data = await response.json();
1155
- if (!cancelled) {
1156
- setPreview(data);
1157
- }
1158
- } catch (err) {
1159
- if (!cancelled) {
1160
- const message = err instanceof Error ? err.message : "Failed to load preview";
1161
- setError(message);
1162
- }
1163
- } finally {
1164
- if (!cancelled) {
1165
- setLoading(false);
1166
- }
1167
- }
1168
- }
1169
- void fetchPreview();
1170
- return () => {
1171
- cancelled = true;
1172
- };
1173
- }, [isOpen, targetPlanId, customerId, apiBaseUrl, apiKey]);
1174
1008
  async function handleConfirm() {
1175
1009
  setConfirming(true);
1010
+ setError(null);
1176
1011
  try {
1177
- const response = await fetch(
1178
- `${apiBaseUrl}/api/v1/subscriptions/change`,
1179
- {
1180
- method: "POST",
1181
- headers: {
1182
- "Content-Type": "application/json",
1183
- Authorization: `Bearer ${apiKey}`
1184
- },
1185
- body: JSON.stringify({ targetPlanId, customerId })
1186
- }
1187
- );
1188
- if (!response.ok) {
1189
- throw new Error(`HTTP ${response.status}: Upgrade failed`);
1012
+ if (!createCheckout) {
1013
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
1190
1014
  }
1191
- onConfirm?.();
1192
- } catch (err) {
1193
- const message = err instanceof Error ? err.message : "Upgrade failed";
1194
- setError(message);
1195
- } finally {
1015
+ const result = await createCheckout({
1016
+ planCode,
1017
+ returnUrl: returnUrl ?? window.location.href
1018
+ });
1019
+ await handleCheckoutResult(result, {
1020
+ onStripeClientSecret: (clientSecret) => {
1021
+ onStripeClientSecret?.(clientSecret);
1022
+ onCheckoutStarted?.();
1023
+ },
1024
+ onComplete: () => onCompleted?.(),
1025
+ onScheduled: () => onScheduled?.()
1026
+ });
1027
+ onConfirm?.();
1028
+ } catch (cause) {
1029
+ const checkoutError = cause instanceof Error ? cause : new Error("Checkout failed");
1030
+ setError(checkoutError.message);
1031
+ onError?.(checkoutError);
1032
+ } finally {
1196
1033
  setConfirming(false);
1197
1034
  }
1198
1035
  }
1199
- if (!isOpen) {
1200
- return null;
1201
- }
1202
- const overlayStyle = {
1203
- position: "fixed",
1204
- inset: 0,
1205
- background: "oklch(0 0 0 / 0.5)",
1206
- display: "flex",
1207
- alignItems: "center",
1208
- justifyContent: "center",
1209
- zIndex: 1e3
1210
- };
1211
- const modalStyle = {
1212
- background: "var(--nozle-background, var(--background))",
1213
- color: "var(--nozle-foreground, var(--foreground))",
1214
- border: "1px solid var(--nozle-border, var(--border))",
1215
- borderRadius: "var(--nozle-radius, 0.5rem)",
1216
- padding: "2rem",
1217
- maxWidth: "28rem",
1218
- width: "100%",
1219
- boxShadow: "var(--nozle-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1))"
1220
- };
1036
+ if (!isOpen) return null;
1221
1037
  return /* @__PURE__ */ jsx(
1222
1038
  "div",
1223
1039
  {
1224
- style: overlayStyle,
1040
+ style: {
1041
+ position: "fixed",
1042
+ inset: 0,
1043
+ background: "oklch(0 0 0 / 0.5)",
1044
+ display: "flex",
1045
+ alignItems: "center",
1046
+ justifyContent: "center",
1047
+ zIndex: 1e3
1048
+ },
1225
1049
  role: "dialog",
1226
1050
  "aria-modal": "true",
1227
- "aria-label": "Upgrade plan",
1228
- children: /* @__PURE__ */ jsxs("div", { style: modalStyle, children: [
1229
- /* @__PURE__ */ jsx(
1230
- "h2",
1231
- {
1232
- style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" },
1233
- children: "Confirm Plan Upgrade"
1234
- }
1235
- ),
1236
- loading && /* @__PURE__ */ jsx(
1237
- "p",
1238
- {
1239
- style: {
1240
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1241
- },
1242
- children: "Loading proration preview..."
1243
- }
1244
- ),
1245
- error && /* @__PURE__ */ jsx("p", { style: { color: "oklch(0.6 0.2 25)", marginBottom: "1rem" }, children: error }),
1246
- preview && !loading && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1247
- /* @__PURE__ */ jsxs(
1248
- "div",
1249
- {
1250
- style: {
1251
- background: "var(--nozle-muted, var(--muted))",
1252
- borderRadius: "var(--nozle-radius, 0.5rem)",
1253
- padding: "1rem",
1254
- display: "flex",
1255
- flexDirection: "column",
1256
- gap: "0.5rem"
1257
- },
1258
- children: [
1259
- preview.credit > 0 && /* @__PURE__ */ jsxs(
1260
- "div",
1261
- {
1262
- style: { display: "flex", justifyContent: "space-between" },
1263
- children: [
1264
- /* @__PURE__ */ jsx("span", { children: "Credit from current plan" }),
1265
- /* @__PURE__ */ jsxs("span", { style: { color: "oklch(0.6 0.15 150)" }, children: [
1266
- "-$",
1267
- preview.credit.toFixed(2)
1268
- ] })
1269
- ]
1270
- }
1271
- ),
1272
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
1273
- /* @__PURE__ */ jsx("span", { children: "New plan charge" }),
1274
- /* @__PURE__ */ jsxs("span", { children: [
1275
- "$",
1276
- preview.debit.toFixed(2)
1277
- ] })
1278
- ] }),
1279
- /* @__PURE__ */ jsxs(
1280
- "div",
1281
- {
1282
- style: {
1283
- display: "flex",
1284
- justifyContent: "space-between",
1285
- fontWeight: 600,
1286
- borderTop: "1px solid var(--nozle-border, var(--border))",
1287
- paddingTop: "0.5rem",
1288
- marginTop: "0.25rem"
1289
- },
1290
- children: [
1291
- /* @__PURE__ */ jsx("span", { children: "You'll be charged today" }),
1292
- /* @__PURE__ */ jsxs("span", { children: [
1293
- "$",
1294
- preview.net.toFixed(2)
1295
- ] })
1296
- ]
1297
- }
1298
- )
1299
- ]
1300
- }
1301
- ),
1302
- /* @__PURE__ */ jsxs(
1303
- "p",
1304
- {
1305
- style: {
1306
- fontSize: "0.875rem",
1307
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
1308
- marginTop: "0.75rem"
1309
- },
1310
- children: [
1311
- "Your next billing date is",
1312
- " ",
1313
- new Date(preview.nextBillingDate).toLocaleDateString(),
1314
- "."
1315
- ]
1316
- }
1317
- )
1318
- ] }),
1319
- /* @__PURE__ */ jsxs(
1320
- "div",
1321
- {
1322
- style: {
1323
- display: "flex",
1324
- gap: "0.75rem",
1325
- justifyContent: "flex-end"
1326
- },
1327
- children: [
1328
- /* @__PURE__ */ jsx(
1329
- "button",
1051
+ "aria-label": "Change plan",
1052
+ children: /* @__PURE__ */ jsxs(
1053
+ "div",
1054
+ {
1055
+ style: {
1056
+ background: "var(--nozle-background, var(--background))",
1057
+ color: "var(--nozle-foreground, var(--foreground))",
1058
+ border: "1px solid var(--nozle-border, var(--border))",
1059
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1060
+ padding: "2rem",
1061
+ maxWidth: "28rem",
1062
+ width: "100%",
1063
+ boxShadow: "var(--nozle-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1))"
1064
+ },
1065
+ children: [
1066
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" }, children: "Confirm plan change" }),
1067
+ error && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: "oklch(0.6 0.2 25)" }, children: error }),
1068
+ preview && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1069
+ /* @__PURE__ */ jsxs(
1070
+ "div",
1330
1071
  {
1331
- onClick: onCancel,
1332
1072
  style: {
1333
- padding: "0.5rem 1rem",
1073
+ background: "var(--nozle-muted, var(--muted))",
1334
1074
  borderRadius: "var(--nozle-radius, 0.5rem)",
1335
- border: "1px solid var(--nozle-border, var(--border))",
1336
- background: "transparent",
1337
- color: "var(--nozle-foreground, var(--foreground))",
1338
- cursor: "pointer"
1075
+ padding: "1rem",
1076
+ display: "flex",
1077
+ flexDirection: "column",
1078
+ gap: "0.5rem"
1339
1079
  },
1340
- disabled: confirming,
1341
- children: "Cancel"
1080
+ children: [
1081
+ preview.credit > 0 && /* @__PURE__ */ jsxs("div", { children: [
1082
+ "Credits applied: -$",
1083
+ preview.credit.toFixed(2)
1084
+ ] }),
1085
+ /* @__PURE__ */ jsxs("div", { children: [
1086
+ "New plan charge: $",
1087
+ preview.debit.toFixed(2)
1088
+ ] }),
1089
+ /* @__PURE__ */ jsxs("strong", { children: [
1090
+ "Due today: $",
1091
+ preview.net.toFixed(2)
1092
+ ] })
1093
+ ]
1342
1094
  }
1343
1095
  ),
1344
- /* @__PURE__ */ jsx(
1345
- "button",
1346
- {
1347
- onClick: () => void handleConfirm(),
1348
- style: {
1349
- padding: "0.5rem 1rem",
1350
- borderRadius: "var(--nozle-radius, 0.5rem)",
1351
- border: "none",
1352
- background: "var(--nozle-primary, var(--primary))",
1353
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1354
- cursor: confirming || loading ? "not-allowed" : "pointer",
1355
- fontWeight: 500,
1356
- opacity: confirming || loading ? 0.7 : 1
1357
- },
1358
- disabled: confirming || loading || !!error,
1359
- children: confirming ? "Upgrading..." : "Confirm Upgrade"
1360
- }
1361
- )
1362
- ]
1363
- }
1364
- )
1365
- ] })
1096
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.875rem" }, children: [
1097
+ "Next billing date: ",
1098
+ new Date(preview.nextBillingDate).toLocaleDateString(),
1099
+ "."
1100
+ ] })
1101
+ ] }),
1102
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", justifyContent: "flex-end" }, children: [
1103
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: confirming, children: "Cancel" }),
1104
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleConfirm(), disabled: confirming, children: confirming ? "Starting checkout..." : "Confirm" })
1105
+ ] })
1106
+ ]
1107
+ }
1108
+ )
1366
1109
  }
1367
1110
  );
1368
1111
  }
1369
1112
  function UpgradeButton({
1370
- targetPlanId,
1113
+ planCode,
1114
+ returnUrl,
1115
+ preview,
1371
1116
  label = "Upgrade",
1372
- apiBaseUrl = "https://api.nozle.app",
1373
1117
  className,
1374
1118
  style,
1375
- onUpgraded
1119
+ onStripeClientSecret,
1120
+ onCheckoutStarted,
1121
+ onDowngradeScheduled,
1122
+ onUpgraded,
1123
+ onError
1376
1124
  }) {
1377
- const { customerId, apiKey } = useBillingPortal();
1378
1125
  const [isOpen, setIsOpen] = useState(false);
1379
1126
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1380
1127
  /* @__PURE__ */ jsx(
1381
1128
  "button",
1382
1129
  {
1130
+ type: "button",
1383
1131
  onClick: () => setIsOpen(true),
1384
1132
  className,
1385
1133
  style: {
@@ -1399,275 +1147,36 @@ function UpgradeButton({
1399
1147
  UpgradeModal,
1400
1148
  {
1401
1149
  isOpen,
1402
- targetPlanId,
1403
- customerId,
1404
- apiBaseUrl,
1405
- apiKey,
1406
- onConfirm: () => {
1150
+ planCode,
1151
+ returnUrl,
1152
+ preview,
1153
+ onStripeClientSecret,
1154
+ onCheckoutStarted: () => {
1407
1155
  setIsOpen(false);
1408
- onUpgraded?.();
1156
+ onCheckoutStarted?.();
1409
1157
  },
1410
- onCancel: () => setIsOpen(false)
1411
- }
1412
- )
1413
- ] });
1414
- }
1415
- var PRESET_AMOUNTS = [10, 25, 50, 100, 250];
1416
- function CreditTopUpButton({
1417
- label = "Add Credits",
1418
- apiBaseUrl = "https://api.nozle.app",
1419
- className,
1420
- style,
1421
- onSuccess,
1422
- onError
1423
- }) {
1424
- const { customerId, apiKey } = useBillingPortal();
1425
- const [isOpen, setIsOpen] = useState(false);
1426
- const [selectedAmount, setSelectedAmount] = useState(25);
1427
- const [customAmount, setCustomAmount] = useState("");
1428
- const [useCustom, setUseCustom] = useState(false);
1429
- const [loading, setLoading] = useState(false);
1430
- const [error, setError] = useState(null);
1431
- const finalAmount = useCustom ? parseFloat(customAmount) || 0 : selectedAmount;
1432
- async function handlePurchase() {
1433
- if (finalAmount <= 0) {
1434
- setError("Please enter a valid amount");
1435
- return;
1436
- }
1437
- setLoading(true);
1438
- setError(null);
1439
- try {
1440
- const response = await fetch(`${apiBaseUrl}/api/v1/credits/purchase`, {
1441
- method: "POST",
1442
- headers: {
1443
- "Content-Type": "application/json",
1444
- Authorization: `Bearer ${apiKey}`
1158
+ onCompleted: () => {
1159
+ setIsOpen(false);
1160
+ onUpgraded?.();
1445
1161
  },
1446
- body: JSON.stringify({ customerId, amount: finalAmount })
1447
- });
1448
- if (!response.ok) {
1449
- throw new Error(`HTTP ${response.status}: Credit purchase failed`);
1450
- }
1451
- setIsOpen(false);
1452
- onSuccess?.(finalAmount);
1453
- } catch (err) {
1454
- const purchaseError = err instanceof Error ? err : new Error("Credit purchase failed");
1455
- setError(purchaseError.message);
1456
- onError?.(purchaseError);
1457
- } finally {
1458
- setLoading(false);
1459
- }
1460
- }
1461
- const overlayStyle = {
1462
- position: "fixed",
1463
- inset: 0,
1464
- background: "oklch(0 0 0 / 0.5)",
1465
- display: "flex",
1466
- alignItems: "center",
1467
- justifyContent: "center",
1468
- zIndex: 1e3
1469
- };
1470
- const dialogStyle = {
1471
- background: "var(--nozle-background, var(--background))",
1472
- color: "var(--nozle-foreground, var(--foreground))",
1473
- border: "1px solid var(--nozle-border, var(--border))",
1474
- borderRadius: "var(--nozle-radius, 0.5rem)",
1475
- padding: "2rem",
1476
- maxWidth: "24rem",
1477
- width: "100%"
1478
- };
1479
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1480
- /* @__PURE__ */ jsx(
1481
- "button",
1482
- {
1483
- onClick: () => setIsOpen(true),
1484
- className,
1485
- style: {
1486
- padding: "0.75rem 1.5rem",
1487
- borderRadius: "var(--nozle-radius, 0.5rem)",
1488
- border: "none",
1489
- background: "var(--nozle-primary, var(--primary))",
1490
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1491
- cursor: "pointer",
1492
- fontWeight: 500,
1493
- ...style
1162
+ onScheduled: () => {
1163
+ setIsOpen(false);
1164
+ onDowngradeScheduled?.();
1494
1165
  },
1495
- children: label
1496
- }
1497
- ),
1498
- isOpen && /* @__PURE__ */ jsx(
1499
- "div",
1500
- {
1501
- style: overlayStyle,
1502
- role: "dialog",
1503
- "aria-modal": "true",
1504
- "aria-label": "Add credits",
1505
- children: /* @__PURE__ */ jsxs("div", { style: dialogStyle, children: [
1506
- /* @__PURE__ */ jsx(
1507
- "h2",
1508
- {
1509
- style: {
1510
- fontSize: "1.25rem",
1511
- fontWeight: 600,
1512
- margin: "0 0 1.25rem"
1513
- },
1514
- children: "Add Credits"
1515
- }
1516
- ),
1517
- /* @__PURE__ */ jsx(
1518
- "div",
1519
- {
1520
- style: {
1521
- display: "flex",
1522
- flexWrap: "wrap",
1523
- gap: "0.5rem",
1524
- marginBottom: "1rem"
1525
- },
1526
- children: PRESET_AMOUNTS.map((amount) => /* @__PURE__ */ jsxs(
1527
- "button",
1528
- {
1529
- onClick: () => {
1530
- setUseCustom(false);
1531
- setSelectedAmount(amount);
1532
- },
1533
- style: {
1534
- padding: "0.5rem 1rem",
1535
- borderRadius: "var(--nozle-radius, 0.5rem)",
1536
- border: "1px solid var(--nozle-border, var(--border))",
1537
- background: !useCustom && selectedAmount === amount ? "var(--nozle-primary, var(--primary))" : "transparent",
1538
- color: !useCustom && selectedAmount === amount ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
1539
- cursor: "pointer"
1540
- },
1541
- "aria-pressed": !useCustom && selectedAmount === amount,
1542
- children: [
1543
- "$",
1544
- amount
1545
- ]
1546
- },
1547
- amount
1548
- ))
1549
- }
1550
- ),
1551
- /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem" }, children: [
1552
- /* @__PURE__ */ jsxs(
1553
- "label",
1554
- {
1555
- style: {
1556
- display: "flex",
1557
- alignItems: "center",
1558
- gap: "0.5rem",
1559
- marginBottom: "0.5rem",
1560
- fontSize: "0.875rem",
1561
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1562
- },
1563
- children: [
1564
- /* @__PURE__ */ jsx(
1565
- "input",
1566
- {
1567
- type: "checkbox",
1568
- checked: useCustom,
1569
- onChange: (e) => setUseCustom(e.target.checked)
1570
- }
1571
- ),
1572
- "Custom amount"
1573
- ]
1574
- }
1575
- ),
1576
- useCustom && /* @__PURE__ */ jsx(
1577
- "input",
1578
- {
1579
- type: "number",
1580
- value: customAmount,
1581
- onChange: (e) => setCustomAmount(e.target.value),
1582
- placeholder: "Enter amount",
1583
- min: 1,
1584
- style: {
1585
- width: "100%",
1586
- padding: "0.5rem 0.75rem",
1587
- borderRadius: "var(--nozle-radius, 0.5rem)",
1588
- border: "1px solid var(--nozle-border, var(--border))",
1589
- background: "var(--nozle-background, var(--background))",
1590
- color: "var(--nozle-foreground, var(--foreground))",
1591
- boxSizing: "border-box"
1592
- }
1593
- }
1594
- )
1595
- ] }),
1596
- error && /* @__PURE__ */ jsx(
1597
- "p",
1598
- {
1599
- style: {
1600
- color: "oklch(0.6 0.2 25)",
1601
- marginBottom: "1rem",
1602
- fontSize: "0.875rem"
1603
- },
1604
- children: error
1605
- }
1606
- ),
1607
- /* @__PURE__ */ jsxs(
1608
- "div",
1609
- {
1610
- style: {
1611
- display: "flex",
1612
- gap: "0.75rem",
1613
- justifyContent: "flex-end"
1614
- },
1615
- children: [
1616
- /* @__PURE__ */ jsx(
1617
- "button",
1618
- {
1619
- onClick: () => setIsOpen(false),
1620
- style: {
1621
- padding: "0.5rem 1rem",
1622
- borderRadius: "var(--nozle-radius, 0.5rem)",
1623
- border: "1px solid var(--nozle-border, var(--border))",
1624
- background: "transparent",
1625
- color: "var(--nozle-foreground, var(--foreground))",
1626
- cursor: "pointer"
1627
- },
1628
- disabled: loading,
1629
- children: "Cancel"
1630
- }
1631
- ),
1632
- /* @__PURE__ */ jsx(
1633
- "button",
1634
- {
1635
- onClick: () => void handlePurchase(),
1636
- style: {
1637
- padding: "0.5rem 1rem",
1638
- borderRadius: "var(--nozle-radius, 0.5rem)",
1639
- border: "none",
1640
- background: "var(--nozle-primary, var(--primary))",
1641
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1642
- cursor: loading ? "not-allowed" : "pointer",
1643
- fontWeight: 500,
1644
- opacity: loading ? 0.7 : 1
1645
- },
1646
- disabled: loading || finalAmount <= 0,
1647
- children: loading ? "Processing..." : `Add $${finalAmount}`
1648
- }
1649
- )
1650
- ]
1651
- }
1652
- )
1653
- ] })
1166
+ onError,
1167
+ onCancel: () => setIsOpen(false)
1654
1168
  }
1655
1169
  )
1656
1170
  ] });
1657
1171
  }
1658
1172
  function FeatureGate({
1659
- customerId,
1660
- feature,
1173
+ allowed,
1174
+ loading = false,
1661
1175
  fallback = null,
1662
1176
  children
1663
1177
  }) {
1664
- const { allowed, loading } = useCan(customerId, feature);
1665
- if (loading) {
1666
- return null;
1667
- }
1668
- if (!allowed) {
1669
- return /* @__PURE__ */ jsx(Fragment, { children: fallback });
1670
- }
1178
+ if (loading) return null;
1179
+ if (!allowed) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
1671
1180
  return /* @__PURE__ */ jsx(Fragment, { children });
1672
1181
  }
1673
1182
  function UsageGate({
@@ -1932,288 +1441,26 @@ function UsageAlert({
1932
1441
  f.key
1933
1442
  )) });
1934
1443
  }
1935
- function CreditBalance({
1936
- customerId,
1937
- currency = "USD"
1938
- }) {
1939
- const { balance, loading, error } = useCredits(customerId);
1940
- if (loading) {
1941
- return /* @__PURE__ */ jsx(
1942
- "div",
1943
- {
1944
- className: "animate-pulse h-8 w-24 rounded",
1945
- style: { background: "var(--nozle-border, var(--border))" }
1946
- }
1947
- );
1948
- }
1949
- if (error) {
1950
- return /* @__PURE__ */ jsx(
1951
- "span",
1952
- {
1953
- style: { color: "var(--nozle-destructive, var(--destructive))" },
1954
- className: "text-sm",
1955
- children: "Failed to load balance"
1956
- }
1957
- );
1958
- }
1959
- const amount = balance ?? 0;
1960
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1961
- /* @__PURE__ */ jsx("span", { "aria-label": "wallet icon", style: { fontSize: "1.25rem" }, role: "img", children: "\u{1F4B3}" }),
1962
- amount === 0 ? /* @__PURE__ */ jsx(
1963
- "span",
1964
- {
1965
- style: {
1966
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1967
- },
1968
- className: "text-sm",
1969
- children: "No credits"
1970
- }
1971
- ) : /* @__PURE__ */ jsx("span", { className: "font-semibold", children: new Intl.NumberFormat("en-US", {
1972
- style: "currency",
1973
- currency,
1974
- minimumFractionDigits: 2
1975
- }).format(amount) })
1976
- ] });
1977
- }
1978
- var TYPE_BADGE_STYLES = {
1979
- grant: {
1980
- background: "var(--nozle-success-bg, #dcfce7)",
1981
- color: "var(--nozle-success, #16a34a)"
1444
+ var TIER_STYLES = {
1445
+ free: {
1446
+ background: "var(--nozle-muted, var(--muted, #f1f5f9))",
1447
+ color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
1448
+ border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
1982
1449
  },
1983
- deduct: {
1984
- background: "var(--nozle-destructive-bg, #fee2e2)",
1985
- color: "var(--nozle-destructive, #dc2626)"
1450
+ starter: {
1451
+ background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
1452
+ color: "var(--nozle-primary, var(--primary, #3b82f6))",
1453
+ border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
1986
1454
  },
1987
- purchase: {
1988
- background: "var(--nozle-info-bg, #dbeafe)",
1989
- color: "var(--nozle-info, #2563eb)"
1455
+ pro: {
1456
+ background: "var(--nozle-primary, var(--primary, #3b82f6))",
1457
+ color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
1458
+ border: "1px solid transparent"
1990
1459
  },
1991
- application: {
1992
- background: "var(--nozle-muted, #f1f5f9)",
1993
- color: "var(--nozle-muted-foreground, #64748b)"
1994
- }
1995
- };
1996
- function getTypeBadgeStyle(type) {
1997
- return TYPE_BADGE_STYLES[type] ?? TYPE_BADGE_STYLES.application;
1998
- }
1999
- function formatDate(iso) {
2000
- try {
2001
- return new Date(iso).toLocaleDateString("en-US", {
2002
- year: "numeric",
2003
- month: "short",
2004
- day: "numeric"
2005
- });
2006
- } catch {
2007
- return iso;
2008
- }
2009
- }
2010
- function CreditHistory({
2011
- customerId,
2012
- viewAllHref = "/billing/credits"
2013
- }) {
2014
- const { client } = useBillingContext();
2015
- const [transactions, setTransactions] = useState([]);
2016
- const [loading, setLoading] = useState(true);
2017
- const [error, setError] = useState(null);
2018
- useEffect(() => {
2019
- if (!client) {
2020
- setError("No client in context");
2021
- setLoading(false);
2022
- return;
2023
- }
2024
- let cancelled = false;
2025
- async function fetchHistory() {
2026
- try {
2027
- const clientInternal = client;
2028
- const apiKey = clientInternal.apiKey ?? "";
2029
- const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
2030
- const timeout = clientInternal.timeout ?? 5e3;
2031
- const controller = new AbortController();
2032
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2033
- const response = await fetch(
2034
- `${baseUrl}/api/v1/credits/${customerId}/transactions`,
2035
- {
2036
- headers: { Authorization: `Bearer ${apiKey}` },
2037
- signal: controller.signal
2038
- }
2039
- );
2040
- clearTimeout(timeoutId);
2041
- if (!response.ok) {
2042
- throw new Error(`HTTP ${response.status}`);
2043
- }
2044
- const data = await response.json();
2045
- if (!cancelled) {
2046
- setTransactions(
2047
- (data.transactions ?? []).slice().sort(
2048
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
2049
- )
2050
- );
2051
- setLoading(false);
2052
- }
2053
- } catch (err) {
2054
- if (!cancelled) {
2055
- setError(err instanceof Error ? err.message : "Unknown error");
2056
- setLoading(false);
2057
- }
2058
- }
2059
- }
2060
- void fetchHistory();
2061
- return () => {
2062
- cancelled = true;
2063
- };
2064
- }, [client, customerId]);
2065
- if (loading) {
2066
- return /* @__PURE__ */ jsx("div", { className: "animate-pulse space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx(
2067
- "div",
2068
- {
2069
- className: "h-6 rounded",
2070
- style: { background: "var(--nozle-border, var(--border))" }
2071
- },
2072
- i
2073
- )) });
2074
- }
2075
- if (error) {
2076
- return /* @__PURE__ */ jsx(
2077
- "span",
2078
- {
2079
- style: { color: "var(--nozle-destructive, var(--destructive))" },
2080
- className: "text-sm",
2081
- children: "Failed to load transactions"
2082
- }
2083
- );
2084
- }
2085
- const displayed = transactions.slice(0, 20);
2086
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2087
- /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
2088
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs(
2089
- "tr",
2090
- {
2091
- style: {
2092
- borderBottom: "1px solid var(--nozle-border, var(--border))",
2093
- fontSize: "0.75rem",
2094
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2095
- },
2096
- children: [
2097
- /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "0.25rem 0.5rem" }, children: "Type" }),
2098
- /* @__PURE__ */ jsx("th", { style: { textAlign: "right", padding: "0.25rem 0.5rem" }, children: "Amount" }),
2099
- /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "0.25rem 0.5rem" }, children: "Description" }),
2100
- /* @__PURE__ */ jsx("th", { style: { textAlign: "right", padding: "0.25rem 0.5rem" }, children: "Date" })
2101
- ]
2102
- }
2103
- ) }),
2104
- /* @__PURE__ */ jsx("tbody", { children: displayed.map((tx) => /* @__PURE__ */ jsxs(
2105
- "tr",
2106
- {
2107
- style: {
2108
- borderBottom: "1px solid var(--nozle-border, var(--border))",
2109
- fontSize: "0.875rem"
2110
- },
2111
- children: [
2112
- /* @__PURE__ */ jsx("td", { style: { padding: "0.5rem" }, children: /* @__PURE__ */ jsx(
2113
- "span",
2114
- {
2115
- style: {
2116
- ...getTypeBadgeStyle(tx.type),
2117
- padding: "0.125rem 0.5rem",
2118
- borderRadius: "9999px",
2119
- fontSize: "0.75rem",
2120
- fontWeight: 500
2121
- },
2122
- children: tx.type
2123
- }
2124
- ) }),
2125
- /* @__PURE__ */ jsxs(
2126
- "td",
2127
- {
2128
- style: {
2129
- textAlign: "right",
2130
- padding: "0.5rem",
2131
- fontVariantNumeric: "tabular-nums",
2132
- color: tx.type === "deduct" ? "var(--nozle-destructive, var(--destructive))" : "var(--nozle-success, #16a34a)"
2133
- },
2134
- children: [
2135
- tx.type === "deduct" ? "-" : "+",
2136
- Math.abs(tx.amount).toLocaleString()
2137
- ]
2138
- }
2139
- ),
2140
- /* @__PURE__ */ jsx(
2141
- "td",
2142
- {
2143
- style: {
2144
- padding: "0.5rem",
2145
- color: "var(--nozle-foreground, var(--foreground))"
2146
- },
2147
- children: tx.description
2148
- }
2149
- ),
2150
- /* @__PURE__ */ jsx(
2151
- "td",
2152
- {
2153
- style: {
2154
- textAlign: "right",
2155
- padding: "0.5rem",
2156
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2157
- fontSize: "0.75rem"
2158
- },
2159
- children: formatDate(tx.createdAt)
2160
- }
2161
- )
2162
- ]
2163
- },
2164
- tx.id
2165
- )) })
2166
- ] }),
2167
- transactions.length > 20 && /* @__PURE__ */ jsx("div", { style: { textAlign: "right" }, children: /* @__PURE__ */ jsxs(
2168
- "a",
2169
- {
2170
- href: viewAllHref,
2171
- style: {
2172
- color: "var(--nozle-primary, var(--primary))",
2173
- fontSize: "0.875rem",
2174
- textDecoration: "underline"
2175
- },
2176
- children: [
2177
- "View all (",
2178
- transactions.length,
2179
- ")"
2180
- ]
2181
- }
2182
- ) }),
2183
- displayed.length === 0 && /* @__PURE__ */ jsx(
2184
- "p",
2185
- {
2186
- style: {
2187
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2188
- fontSize: "0.875rem",
2189
- textAlign: "center",
2190
- padding: "1rem"
2191
- },
2192
- children: "No credit transactions yet."
2193
- }
2194
- )
2195
- ] });
2196
- }
2197
- var TIER_STYLES = {
2198
- free: {
2199
- background: "var(--nozle-muted, var(--muted, #f1f5f9))",
2200
- color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
2201
- border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
2202
- },
2203
- starter: {
2204
- background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
2205
- color: "var(--nozle-primary, var(--primary, #3b82f6))",
2206
- border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
2207
- },
2208
- pro: {
2209
- background: "var(--nozle-primary, var(--primary, #3b82f6))",
2210
- color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
2211
- border: "1px solid transparent"
2212
- },
2213
- enterprise: {
2214
- background: "var(--nozle-accent, var(--accent, #8b5cf6))",
2215
- color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
2216
- border: "1px solid transparent"
1460
+ enterprise: {
1461
+ background: "var(--nozle-accent, var(--accent, #8b5cf6))",
1462
+ color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
1463
+ border: "1px solid transparent"
2217
1464
  }
2218
1465
  };
2219
1466
  function PlanBadge({ plan, tier = "free" }) {
@@ -2236,429 +1483,6 @@ function PlanBadge({ plan, tier = "free" }) {
2236
1483
  }
2237
1484
  );
2238
1485
  }
2239
- function formatDate2(iso) {
2240
- if (!iso) return "\u2014";
2241
- try {
2242
- return new Date(iso).toLocaleDateString("en-US", {
2243
- year: "numeric",
2244
- month: "long",
2245
- day: "numeric"
2246
- });
2247
- } catch {
2248
- return iso;
2249
- }
2250
- }
2251
- function CurrentPlan({
2252
- customerId,
2253
- onChangePlan,
2254
- changePlanHref = "/plans"
2255
- }) {
2256
- const { client } = useBillingContext();
2257
- const [plan, setPlan] = useState(null);
2258
- const [loading, setLoading] = useState(true);
2259
- const [error, setError] = useState(null);
2260
- useEffect(() => {
2261
- if (!client) {
2262
- setError("No client in context");
2263
- setLoading(false);
2264
- return;
2265
- }
2266
- let cancelled = false;
2267
- async function fetchPlan() {
2268
- try {
2269
- const clientInternal = client;
2270
- const apiKey = clientInternal.apiKey ?? "";
2271
- const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
2272
- const timeout = clientInternal.timeout ?? 5e3;
2273
- const controller = new AbortController();
2274
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2275
- const response = await fetch(
2276
- `${baseUrl}/api/v1/subscriptions/current?customer_id=${customerId}`,
2277
- {
2278
- headers: { Authorization: `Bearer ${apiKey}` },
2279
- signal: controller.signal
2280
- }
2281
- );
2282
- clearTimeout(timeoutId);
2283
- if (!response.ok) {
2284
- throw new Error(`HTTP ${response.status}`);
2285
- }
2286
- const data = await response.json();
2287
- if (!cancelled) {
2288
- setPlan(data);
2289
- setLoading(false);
2290
- }
2291
- } catch (err) {
2292
- if (!cancelled) {
2293
- setError(err instanceof Error ? err.message : "Unknown error");
2294
- setLoading(false);
2295
- }
2296
- }
2297
- }
2298
- void fetchPlan();
2299
- return () => {
2300
- cancelled = true;
2301
- };
2302
- }, [client, customerId]);
2303
- if (loading) {
2304
- return /* @__PURE__ */ jsxs("div", { className: "animate-pulse space-y-2", children: [
2305
- /* @__PURE__ */ jsx(
2306
- "div",
2307
- {
2308
- className: "h-6 w-32 rounded",
2309
- style: { background: "var(--nozle-border, var(--border))" }
2310
- }
2311
- ),
2312
- /* @__PURE__ */ jsx(
2313
- "div",
2314
- {
2315
- className: "h-4 w-48 rounded",
2316
- style: { background: "var(--nozle-border, var(--border))" }
2317
- }
2318
- )
2319
- ] });
2320
- }
2321
- if (error || !plan) {
2322
- return /* @__PURE__ */ jsx(
2323
- "span",
2324
- {
2325
- style: { color: "var(--nozle-destructive, var(--destructive))" },
2326
- className: "text-sm",
2327
- children: "Failed to load current plan"
2328
- }
2329
- );
2330
- }
2331
- const handleChangePlan = () => {
2332
- if (onChangePlan) {
2333
- onChangePlan();
2334
- } else {
2335
- window.location.href = changePlanHref;
2336
- }
2337
- };
2338
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2339
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2340
- /* @__PURE__ */ jsx(PlanBadge, { plan: plan.planName, tier: plan.tier }),
2341
- /* @__PURE__ */ jsx(
2342
- "span",
2343
- {
2344
- style: {
2345
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2346
- fontSize: "0.875rem"
2347
- },
2348
- children: plan.interval
2349
- }
2350
- ),
2351
- /* @__PURE__ */ jsx(
2352
- "span",
2353
- {
2354
- style: {
2355
- color: plan.status === "active" ? "var(--nozle-success, #16a34a)" : "var(--nozle-warning, var(--warning))",
2356
- fontSize: "0.875rem",
2357
- fontWeight: 500
2358
- },
2359
- children: plan.status
2360
- }
2361
- )
2362
- ] }),
2363
- plan.nextBillingDate && /* @__PURE__ */ jsxs(
2364
- "p",
2365
- {
2366
- style: {
2367
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2368
- fontSize: "0.875rem"
2369
- },
2370
- children: [
2371
- "Next billing: ",
2372
- formatDate2(plan.nextBillingDate)
2373
- ]
2374
- }
2375
- ),
2376
- /* @__PURE__ */ jsx(
2377
- "button",
2378
- {
2379
- onClick: handleChangePlan,
2380
- style: {
2381
- background: "transparent",
2382
- border: "1px solid var(--nozle-primary, var(--primary))",
2383
- color: "var(--nozle-primary, var(--primary))",
2384
- padding: "0.375rem 0.875rem",
2385
- borderRadius: "0.375rem",
2386
- fontSize: "0.875rem",
2387
- fontWeight: 500,
2388
- cursor: "pointer"
2389
- },
2390
- children: "Change Plan"
2391
- }
2392
- )
2393
- ] });
2394
- }
2395
- var STATUS_STYLES = {
2396
- paid: {
2397
- background: "var(--nozle-success-bg, #dcfce7)",
2398
- color: "var(--nozle-success, #16a34a)"
2399
- },
2400
- open: {
2401
- background: "var(--nozle-info-bg, #dbeafe)",
2402
- color: "var(--nozle-info, #2563eb)"
2403
- },
2404
- draft: {
2405
- background: "var(--nozle-muted, #f1f5f9)",
2406
- color: "var(--nozle-muted-foreground, #64748b)"
2407
- },
2408
- void: {
2409
- background: "var(--nozle-muted, #f1f5f9)",
2410
- color: "var(--nozle-muted-foreground, #64748b)"
2411
- },
2412
- uncollectible: {
2413
- background: "var(--nozle-destructive-bg, #fee2e2)",
2414
- color: "var(--nozle-destructive, #dc2626)"
2415
- }
2416
- };
2417
- function formatDate3(iso) {
2418
- try {
2419
- return new Date(iso).toLocaleDateString("en-US", {
2420
- year: "numeric",
2421
- month: "short",
2422
- day: "numeric"
2423
- });
2424
- } catch {
2425
- return iso;
2426
- }
2427
- }
2428
- function InvoiceList({ customerId }) {
2429
- const { client } = useBillingContext();
2430
- const [invoices, setInvoices] = useState([]);
2431
- const [loading, setLoading] = useState(true);
2432
- const [error, setError] = useState(null);
2433
- useEffect(() => {
2434
- if (!client) {
2435
- setError("No client in context");
2436
- setLoading(false);
2437
- return;
2438
- }
2439
- let cancelled = false;
2440
- async function fetchInvoices() {
2441
- try {
2442
- const clientInternal = client;
2443
- const apiKey = clientInternal.apiKey ?? "";
2444
- const baseUrl = clientInternal.baseUrl ?? "https://api.nozle.app";
2445
- const timeout = clientInternal.timeout ?? 5e3;
2446
- const controller = new AbortController();
2447
- const timeoutId = setTimeout(() => controller.abort(), timeout);
2448
- const response = await fetch(
2449
- `${baseUrl}/api/v1/invoices?customer_id=${customerId}`,
2450
- {
2451
- headers: { Authorization: `Bearer ${apiKey}` },
2452
- signal: controller.signal
2453
- }
2454
- );
2455
- clearTimeout(timeoutId);
2456
- if (!response.ok) {
2457
- throw new Error(`HTTP ${response.status}`);
2458
- }
2459
- const data = await response.json();
2460
- if (!cancelled) {
2461
- setInvoices(data.invoices ?? data.data ?? []);
2462
- setLoading(false);
2463
- }
2464
- } catch (err) {
2465
- if (!cancelled) {
2466
- setError(err instanceof Error ? err.message : "Unknown error");
2467
- setLoading(false);
2468
- }
2469
- }
2470
- }
2471
- void fetchInvoices();
2472
- return () => {
2473
- cancelled = true;
2474
- };
2475
- }, [client, customerId]);
2476
- if (loading) {
2477
- return /* @__PURE__ */ jsx("div", { className: "animate-pulse space-y-2", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsx(
2478
- "div",
2479
- {
2480
- className: "h-8 rounded",
2481
- style: { background: "var(--nozle-border, var(--border))" }
2482
- },
2483
- i
2484
- )) });
2485
- }
2486
- if (error) {
2487
- return /* @__PURE__ */ jsx(
2488
- "span",
2489
- {
2490
- style: { color: "var(--nozle-destructive, var(--destructive))" },
2491
- className: "text-sm",
2492
- children: "Failed to load invoices"
2493
- }
2494
- );
2495
- }
2496
- if (invoices.length === 0) {
2497
- return /* @__PURE__ */ jsx(
2498
- "p",
2499
- {
2500
- style: {
2501
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2502
- fontSize: "0.875rem",
2503
- textAlign: "center",
2504
- padding: "1.5rem"
2505
- },
2506
- children: "No invoices yet."
2507
- }
2508
- );
2509
- }
2510
- return /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
2511
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs(
2512
- "tr",
2513
- {
2514
- style: {
2515
- borderBottom: "1px solid var(--nozle-border, var(--border))",
2516
- fontSize: "0.75rem",
2517
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2518
- },
2519
- children: [
2520
- /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "0.5rem" }, children: "#" }),
2521
- /* @__PURE__ */ jsx("th", { style: { textAlign: "left", padding: "0.5rem" }, children: "Date" }),
2522
- /* @__PURE__ */ jsx("th", { style: { textAlign: "right", padding: "0.5rem" }, children: "Amount" }),
2523
- /* @__PURE__ */ jsx("th", { style: { textAlign: "center", padding: "0.5rem" }, children: "Status" }),
2524
- /* @__PURE__ */ jsx("th", { style: { textAlign: "center", padding: "0.5rem" }, children: "PDF" })
2525
- ]
2526
- }
2527
- ) }),
2528
- /* @__PURE__ */ jsx("tbody", { children: invoices.map((inv) => /* @__PURE__ */ jsxs(
2529
- "tr",
2530
- {
2531
- style: {
2532
- borderBottom: "1px solid var(--nozle-border, var(--border))",
2533
- fontSize: "0.875rem"
2534
- },
2535
- children: [
2536
- /* @__PURE__ */ jsx(
2537
- "td",
2538
- {
2539
- style: {
2540
- padding: "0.5rem",
2541
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
2542
- },
2543
- children: inv.number
2544
- }
2545
- ),
2546
- /* @__PURE__ */ jsx("td", { style: { padding: "0.5rem" }, children: formatDate3(inv.date) }),
2547
- /* @__PURE__ */ jsx(
2548
- "td",
2549
- {
2550
- style: {
2551
- textAlign: "right",
2552
- padding: "0.5rem",
2553
- fontVariantNumeric: "tabular-nums"
2554
- },
2555
- children: new Intl.NumberFormat("en-US", {
2556
- style: "currency",
2557
- currency: inv.currency ?? "USD"
2558
- }).format(inv.amount / 100)
2559
- }
2560
- ),
2561
- /* @__PURE__ */ jsx("td", { style: { textAlign: "center", padding: "0.5rem" }, children: /* @__PURE__ */ jsx(
2562
- "span",
2563
- {
2564
- style: {
2565
- ...STATUS_STYLES[inv.status] ?? STATUS_STYLES.draft,
2566
- padding: "0.125rem 0.5rem",
2567
- borderRadius: "9999px",
2568
- fontSize: "0.75rem",
2569
- fontWeight: 500
2570
- },
2571
- children: inv.status
2572
- }
2573
- ) }),
2574
- /* @__PURE__ */ jsx("td", { style: { textAlign: "center", padding: "0.5rem" }, children: inv.pdf_url ? /* @__PURE__ */ jsx(
2575
- "a",
2576
- {
2577
- href: inv.pdf_url,
2578
- target: "_blank",
2579
- rel: "noopener noreferrer",
2580
- "aria-label": `Download invoice ${inv.number}`,
2581
- style: {
2582
- color: "var(--nozle-primary, var(--primary))",
2583
- textDecoration: "underline",
2584
- fontSize: "0.75rem"
2585
- },
2586
- children: "Download"
2587
- }
2588
- ) : /* @__PURE__ */ jsx(
2589
- "span",
2590
- {
2591
- style: {
2592
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2593
- fontSize: "0.75rem"
2594
- },
2595
- children: "\u2014"
2596
- }
2597
- ) })
2598
- ]
2599
- },
2600
- inv.id
2601
- )) })
2602
- ] });
2603
- }
2604
- function Section({
2605
- title,
2606
- children
2607
- }) {
2608
- return /* @__PURE__ */ jsxs(
2609
- "div",
2610
- {
2611
- style: {
2612
- padding: "1.25rem",
2613
- border: "1px solid var(--nozle-border, var(--border))",
2614
- borderRadius: "0.75rem",
2615
- background: "var(--nozle-card, var(--card, #ffffff))"
2616
- },
2617
- children: [
2618
- /* @__PURE__ */ jsx(
2619
- "h3",
2620
- {
2621
- style: {
2622
- fontSize: "0.875rem",
2623
- fontWeight: 600,
2624
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2625
- textTransform: "uppercase",
2626
- letterSpacing: "0.05em",
2627
- marginBottom: "1rem"
2628
- },
2629
- children: title
2630
- }
2631
- ),
2632
- children
2633
- ]
2634
- }
2635
- );
2636
- }
2637
- function BillingPortal({
2638
- customerId,
2639
- usageFeatures,
2640
- usageLoading = false,
2641
- onChangePlan
2642
- }) {
2643
- return /* @__PURE__ */ jsxs(
2644
- "div",
2645
- {
2646
- style: {
2647
- display: "flex",
2648
- flexDirection: "column",
2649
- gap: "1.25rem",
2650
- fontFamily: "var(--nozle-font-sans, var(--font-sans, system-ui, sans-serif))",
2651
- color: "var(--nozle-foreground, var(--foreground))"
2652
- },
2653
- children: [
2654
- /* @__PURE__ */ jsx(Section, { title: "Current Plan", children: /* @__PURE__ */ jsx(CurrentPlan, { customerId, onChangePlan }) }),
2655
- /* @__PURE__ */ jsx(Section, { title: "Usage", children: /* @__PURE__ */ jsx(UsageDashboard, { features: usageFeatures, loading: usageLoading }) }),
2656
- /* @__PURE__ */ jsx(Section, { title: "Invoices", children: /* @__PURE__ */ jsx(InvoiceList, { customerId }) }),
2657
- /* @__PURE__ */ jsx(Section, { title: "Credits", children: /* @__PURE__ */ jsx(CreditBalance, { customerId }) })
2658
- ]
2659
- }
2660
- );
2661
- }
2662
1486
  var BRAND_LABELS = {
2663
1487
  visa: "Visa",
2664
1488
  mastercard: "Mastercard",
@@ -2762,327 +1586,7 @@ function PaymentMethodDisplay({
2762
1586
  )
2763
1587
  ] });
2764
1588
  }
2765
- var CANCEL_REASONS = [
2766
- "Too expensive",
2767
- "Missing features",
2768
- "Switching to competitor",
2769
- "Not using it enough",
2770
- "Other"
2771
- ];
2772
- function CancelSubscriptionButton({
2773
- subscriptionId,
2774
- onCancelled
2775
- }) {
2776
- const [step, setStep] = useState("idle");
2777
- const [selectedReason, setSelectedReason] = useState("");
2778
- const [otherText, setOtherText] = useState("");
2779
- const [loading, setLoading] = useState(false);
2780
- const handleDismiss = () => {
2781
- setStep("idle");
2782
- setSelectedReason("");
2783
- setOtherText("");
2784
- };
2785
- const handleConfirmCancel = () => {
2786
- setStep("reason");
2787
- };
2788
- const handleSubmitCancellation = async () => {
2789
- const reason = selectedReason === "Other" ? otherText : selectedReason;
2790
- setLoading(true);
2791
- await fetch(`/api/v1/subscriptions/${subscriptionId}`, {
2792
- method: "DELETE",
2793
- headers: { "Content-Type": "application/json" },
2794
- body: JSON.stringify({ reason })
2795
- });
2796
- setLoading(false);
2797
- handleDismiss();
2798
- onCancelled?.();
2799
- };
2800
- const isSubmitDisabled = loading || !selectedReason || selectedReason === "Other" && !otherText.trim();
2801
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2802
- /* @__PURE__ */ jsx(
2803
- "button",
2804
- {
2805
- onClick: () => setStep("confirm"),
2806
- style: {
2807
- background: "transparent",
2808
- border: "1px solid var(--nozle-destructive, var(--destructive, #dc2626))",
2809
- color: "var(--nozle-destructive, var(--destructive, #dc2626))",
2810
- padding: "0.375rem 0.875rem",
2811
- borderRadius: "0.375rem",
2812
- fontSize: "0.875rem",
2813
- fontWeight: 500,
2814
- cursor: "pointer"
2815
- },
2816
- children: "Cancel Subscription"
2817
- }
2818
- ),
2819
- step === "confirm" && /* @__PURE__ */ jsx(
2820
- "div",
2821
- {
2822
- role: "dialog",
2823
- "aria-modal": "true",
2824
- "aria-label": "Cancel subscription confirmation",
2825
- style: {
2826
- position: "fixed",
2827
- inset: 0,
2828
- display: "flex",
2829
- alignItems: "center",
2830
- justifyContent: "center",
2831
- background: "rgba(0,0,0,0.4)",
2832
- zIndex: 50
2833
- },
2834
- children: /* @__PURE__ */ jsxs(
2835
- "div",
2836
- {
2837
- style: {
2838
- background: "var(--nozle-background, var(--background, #fff))",
2839
- border: "1px solid var(--nozle-border, var(--border))",
2840
- borderRadius: "0.75rem",
2841
- padding: "1.5rem",
2842
- maxWidth: "28rem",
2843
- width: "90%",
2844
- boxShadow: "0 20px 60px rgba(0,0,0,0.15)"
2845
- },
2846
- children: [
2847
- /* @__PURE__ */ jsx(
2848
- "h2",
2849
- {
2850
- style: {
2851
- fontSize: "1.125rem",
2852
- fontWeight: 600,
2853
- marginBottom: "0.75rem"
2854
- },
2855
- children: "Cancel Subscription?"
2856
- }
2857
- ),
2858
- /* @__PURE__ */ jsx(
2859
- "p",
2860
- {
2861
- style: {
2862
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2863
- fontSize: "0.875rem",
2864
- marginBottom: "1.5rem"
2865
- },
2866
- children: "Your access will end at the end of the billing period."
2867
- }
2868
- ),
2869
- /* @__PURE__ */ jsxs(
2870
- "div",
2871
- {
2872
- style: {
2873
- display: "flex",
2874
- gap: "0.75rem",
2875
- justifyContent: "flex-end"
2876
- },
2877
- children: [
2878
- /* @__PURE__ */ jsx(
2879
- "button",
2880
- {
2881
- onClick: handleDismiss,
2882
- style: {
2883
- background: "transparent",
2884
- border: "1px solid var(--nozle-border, var(--border))",
2885
- color: "var(--nozle-foreground, var(--foreground))",
2886
- padding: "0.375rem 0.875rem",
2887
- borderRadius: "0.375rem",
2888
- fontSize: "0.875rem",
2889
- cursor: "pointer"
2890
- },
2891
- children: "Keep Subscription"
2892
- }
2893
- ),
2894
- /* @__PURE__ */ jsx(
2895
- "button",
2896
- {
2897
- onClick: handleConfirmCancel,
2898
- style: {
2899
- background: "var(--nozle-destructive, var(--destructive, #dc2626))",
2900
- border: "1px solid transparent",
2901
- color: "#fff",
2902
- padding: "0.375rem 0.875rem",
2903
- borderRadius: "0.375rem",
2904
- fontSize: "0.875rem",
2905
- fontWeight: 500,
2906
- cursor: "pointer"
2907
- },
2908
- children: "Confirm Cancel"
2909
- }
2910
- )
2911
- ]
2912
- }
2913
- )
2914
- ]
2915
- }
2916
- )
2917
- }
2918
- ),
2919
- step === "reason" && /* @__PURE__ */ jsx(
2920
- "div",
2921
- {
2922
- role: "dialog",
2923
- "aria-modal": "true",
2924
- "aria-label": "Cancellation reason survey",
2925
- style: {
2926
- position: "fixed",
2927
- inset: 0,
2928
- display: "flex",
2929
- alignItems: "center",
2930
- justifyContent: "center",
2931
- background: "rgba(0,0,0,0.4)",
2932
- zIndex: 50
2933
- },
2934
- children: /* @__PURE__ */ jsxs(
2935
- "div",
2936
- {
2937
- style: {
2938
- background: "var(--nozle-background, var(--background, #fff))",
2939
- border: "1px solid var(--nozle-border, var(--border))",
2940
- borderRadius: "0.75rem",
2941
- padding: "1.5rem",
2942
- maxWidth: "28rem",
2943
- width: "90%",
2944
- boxShadow: "0 20px 60px rgba(0,0,0,0.15)"
2945
- },
2946
- children: [
2947
- /* @__PURE__ */ jsx(
2948
- "h2",
2949
- {
2950
- style: {
2951
- fontSize: "1.125rem",
2952
- fontWeight: 600,
2953
- marginBottom: "0.5rem"
2954
- },
2955
- children: "Why are you cancelling?"
2956
- }
2957
- ),
2958
- /* @__PURE__ */ jsx(
2959
- "p",
2960
- {
2961
- style: {
2962
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
2963
- fontSize: "0.875rem",
2964
- marginBottom: "1rem"
2965
- },
2966
- children: "Your feedback helps us improve."
2967
- }
2968
- ),
2969
- /* @__PURE__ */ jsxs(
2970
- "fieldset",
2971
- {
2972
- style: {
2973
- border: "none",
2974
- padding: 0,
2975
- margin: 0,
2976
- marginBottom: "1rem"
2977
- },
2978
- children: [
2979
- /* @__PURE__ */ jsx("legend", { className: "sr-only", children: "Cancellation reason" }),
2980
- CANCEL_REASONS.map((r) => /* @__PURE__ */ jsxs(
2981
- "label",
2982
- {
2983
- style: {
2984
- display: "flex",
2985
- alignItems: "center",
2986
- gap: "0.5rem",
2987
- padding: "0.375rem 0",
2988
- fontSize: "0.875rem",
2989
- cursor: "pointer"
2990
- },
2991
- children: [
2992
- /* @__PURE__ */ jsx(
2993
- "input",
2994
- {
2995
- type: "radio",
2996
- name: "cancel-reason",
2997
- value: r,
2998
- checked: selectedReason === r,
2999
- onChange: () => setSelectedReason(r),
3000
- "aria-label": r
3001
- }
3002
- ),
3003
- r
3004
- ]
3005
- },
3006
- r
3007
- ))
3008
- ]
3009
- }
3010
- ),
3011
- selectedReason === "Other" && /* @__PURE__ */ jsx(
3012
- "input",
3013
- {
3014
- type: "text",
3015
- placeholder: "Please describe your reason",
3016
- value: otherText,
3017
- onChange: (e) => setOtherText(e.target.value),
3018
- "aria-label": "Other reason",
3019
- style: {
3020
- width: "100%",
3021
- border: "1px solid var(--nozle-border, var(--border))",
3022
- borderRadius: "0.375rem",
3023
- padding: "0.5rem 0.75rem",
3024
- fontSize: "0.875rem",
3025
- marginBottom: "1rem",
3026
- background: "var(--nozle-background, var(--background, #fff))",
3027
- color: "var(--nozle-foreground, var(--foreground))",
3028
- boxSizing: "border-box"
3029
- }
3030
- }
3031
- ),
3032
- /* @__PURE__ */ jsxs(
3033
- "div",
3034
- {
3035
- style: {
3036
- display: "flex",
3037
- gap: "0.75rem",
3038
- justifyContent: "flex-end"
3039
- },
3040
- children: [
3041
- /* @__PURE__ */ jsx(
3042
- "button",
3043
- {
3044
- onClick: handleDismiss,
3045
- style: {
3046
- background: "transparent",
3047
- border: "1px solid var(--nozle-border, var(--border))",
3048
- color: "var(--nozle-foreground, var(--foreground))",
3049
- padding: "0.375rem 0.875rem",
3050
- borderRadius: "0.375rem",
3051
- fontSize: "0.875rem",
3052
- cursor: "pointer"
3053
- },
3054
- children: "Keep Subscription"
3055
- }
3056
- ),
3057
- /* @__PURE__ */ jsx(
3058
- "button",
3059
- {
3060
- onClick: handleSubmitCancellation,
3061
- disabled: isSubmitDisabled,
3062
- style: {
3063
- background: isSubmitDisabled ? "var(--nozle-muted, var(--muted, #f1f5f9))" : "var(--nozle-destructive, var(--destructive, #dc2626))",
3064
- border: "1px solid transparent",
3065
- color: isSubmitDisabled ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "#fff",
3066
- padding: "0.375rem 0.875rem",
3067
- borderRadius: "0.375rem",
3068
- fontSize: "0.875rem",
3069
- fontWeight: 500,
3070
- cursor: isSubmitDisabled ? "not-allowed" : "pointer"
3071
- },
3072
- children: loading ? "Cancelling..." : "Submit Cancellation"
3073
- }
3074
- )
3075
- ]
3076
- }
3077
- )
3078
- ]
3079
- }
3080
- )
3081
- }
3082
- )
3083
- ] });
3084
- }
3085
1589
 
3086
- 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 };
1590
+ export { BillingContext, BillingProvider, Checkout, CheckoutButton, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanComparison, PlanGate, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageDashboard, UsageGate, UsageMeter, getUsageMeterColor, useBillingContext, useCheckout2 as useCheckout, useCheckout as useCheckoutSession, useNozleClient, useOptionalBillingContext, usePlans };
3087
1591
  //# sourceMappingURL=index.js.map
3088
1592
  //# sourceMappingURL=index.js.map