@nozle-js/react 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,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,8 @@ function CheckoutInner({
1009
872
  onError,
1010
873
  onReady,
1011
874
  className,
1012
- style
875
+ style,
876
+ children
1013
877
  }) {
1014
878
  const stripe = useStripe();
1015
879
  const elements = useElements();
@@ -1059,7 +923,7 @@ function CheckoutInner({
1059
923
  }
1060
924
  ),
1061
925
  error !== null && /* @__PURE__ */ jsx("p", { style: { color: "var(--nozle-destructive, #dc2626)", margin: "0.5rem 0 0" }, children: error.message }),
1062
- /* @__PURE__ */ jsx(
926
+ children ?? /* @__PURE__ */ jsx(
1063
927
  "button",
1064
928
  {
1065
929
  type: "button",
@@ -1089,16 +953,25 @@ function Checkout({
1089
953
  stripeAccount,
1090
954
  submitLabel,
1091
955
  onSuccess,
956
+ onComplete,
1092
957
  onError,
1093
958
  onReady,
1094
959
  className,
1095
- style
960
+ style,
961
+ children
1096
962
  }) {
1097
963
  const stripePromise = useMemo(
1098
964
  () => loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1099
965
  [publishableKey, stripeAccount]
1100
966
  );
1101
967
  const appearance = useMemo(() => buildStripeAppearance(), []);
968
+ const embeddedOptions = useMemo(
969
+ () => ({ clientSecret, onComplete }),
970
+ [clientSecret, onComplete]
971
+ );
972
+ if (clientSecret.startsWith("cs_")) {
973
+ return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(EmbeddedCheckoutProvider, { stripe: stripePromise, options: embeddedOptions, children: /* @__PURE__ */ jsx(EmbeddedCheckout, {}) }) });
974
+ }
1102
975
  const options = { clientSecret, appearance };
1103
976
  return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
1104
977
  CheckoutInner,
@@ -1108,278 +981,150 @@ function Checkout({
1108
981
  onError,
1109
982
  onReady,
1110
983
  className,
1111
- style
984
+ style,
985
+ children
1112
986
  }
1113
987
  ) }, clientSecret);
1114
988
  }
1115
989
  function UpgradeModal({
1116
990
  isOpen,
1117
- targetPlanId,
1118
- customerId,
1119
- apiBaseUrl = "https://api.nozle.app",
1120
- apiKey = "",
991
+ planCode,
992
+ returnUrl,
993
+ preview,
994
+ onStripeClientSecret,
995
+ onCheckoutStarted,
996
+ onCompleted,
997
+ onScheduled,
1121
998
  onConfirm,
1122
- onCancel
999
+ onCancel,
1000
+ onError
1123
1001
  }) {
1124
- const [preview, setPreview] = useState(null);
1125
- const [loading, setLoading] = useState(false);
1002
+ const { createCheckout } = useBillingContext();
1126
1003
  const [error, setError] = useState(null);
1127
1004
  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
1005
  async function handleConfirm() {
1175
1006
  setConfirming(true);
1007
+ setError(null);
1176
1008
  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`);
1009
+ if (!createCheckout) {
1010
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
1190
1011
  }
1191
- onConfirm?.();
1192
- } catch (err) {
1193
- const message = err instanceof Error ? err.message : "Upgrade failed";
1194
- setError(message);
1195
- } finally {
1012
+ const result = await createCheckout({
1013
+ planCode,
1014
+ returnUrl: returnUrl ?? window.location.href
1015
+ });
1016
+ await handleCheckoutResult(result, {
1017
+ onStripeClientSecret: (clientSecret) => {
1018
+ onStripeClientSecret?.(clientSecret);
1019
+ onCheckoutStarted?.();
1020
+ },
1021
+ onComplete: () => onCompleted?.(),
1022
+ onScheduled: () => onScheduled?.()
1023
+ });
1024
+ onConfirm?.();
1025
+ } catch (cause) {
1026
+ const checkoutError = cause instanceof Error ? cause : new Error("Checkout failed");
1027
+ setError(checkoutError.message);
1028
+ onError?.(checkoutError);
1029
+ } finally {
1196
1030
  setConfirming(false);
1197
1031
  }
1198
1032
  }
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
- };
1033
+ if (!isOpen) return null;
1221
1034
  return /* @__PURE__ */ jsx(
1222
1035
  "div",
1223
1036
  {
1224
- style: overlayStyle,
1037
+ style: {
1038
+ position: "fixed",
1039
+ inset: 0,
1040
+ background: "oklch(0 0 0 / 0.5)",
1041
+ display: "flex",
1042
+ alignItems: "center",
1043
+ justifyContent: "center",
1044
+ zIndex: 1e3
1045
+ },
1225
1046
  role: "dialog",
1226
1047
  "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",
1048
+ "aria-label": "Change plan",
1049
+ children: /* @__PURE__ */ jsxs(
1050
+ "div",
1051
+ {
1052
+ style: {
1053
+ background: "var(--nozle-background, var(--background))",
1054
+ color: "var(--nozle-foreground, var(--foreground))",
1055
+ border: "1px solid var(--nozle-border, var(--border))",
1056
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1057
+ padding: "2rem",
1058
+ maxWidth: "28rem",
1059
+ width: "100%",
1060
+ boxShadow: "var(--nozle-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1))"
1061
+ },
1062
+ children: [
1063
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" }, children: "Confirm plan change" }),
1064
+ error && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: "oklch(0.6 0.2 25)" }, children: error }),
1065
+ preview && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1066
+ /* @__PURE__ */ jsxs(
1067
+ "div",
1330
1068
  {
1331
- onClick: onCancel,
1332
1069
  style: {
1333
- padding: "0.5rem 1rem",
1070
+ background: "var(--nozle-muted, var(--muted))",
1334
1071
  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"
1072
+ padding: "1rem",
1073
+ display: "flex",
1074
+ flexDirection: "column",
1075
+ gap: "0.5rem"
1339
1076
  },
1340
- disabled: confirming,
1341
- children: "Cancel"
1077
+ children: [
1078
+ preview.credit > 0 && /* @__PURE__ */ jsxs("div", { children: [
1079
+ "Credits applied: -$",
1080
+ preview.credit.toFixed(2)
1081
+ ] }),
1082
+ /* @__PURE__ */ jsxs("div", { children: [
1083
+ "New plan charge: $",
1084
+ preview.debit.toFixed(2)
1085
+ ] }),
1086
+ /* @__PURE__ */ jsxs("strong", { children: [
1087
+ "Due today: $",
1088
+ preview.net.toFixed(2)
1089
+ ] })
1090
+ ]
1342
1091
  }
1343
1092
  ),
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
- ] })
1093
+ /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.875rem" }, children: [
1094
+ "Next billing date: ",
1095
+ new Date(preview.nextBillingDate).toLocaleDateString(),
1096
+ "."
1097
+ ] })
1098
+ ] }),
1099
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", justifyContent: "flex-end" }, children: [
1100
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: confirming, children: "Cancel" }),
1101
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void handleConfirm(), disabled: confirming, children: confirming ? "Starting checkout..." : "Confirm" })
1102
+ ] })
1103
+ ]
1104
+ }
1105
+ )
1366
1106
  }
1367
1107
  );
1368
1108
  }
1369
1109
  function UpgradeButton({
1370
- targetPlanId,
1110
+ planCode,
1111
+ returnUrl,
1112
+ preview,
1371
1113
  label = "Upgrade",
1372
- apiBaseUrl = "https://api.nozle.app",
1373
1114
  className,
1374
1115
  style,
1375
- onUpgraded
1116
+ onStripeClientSecret,
1117
+ onCheckoutStarted,
1118
+ onDowngradeScheduled,
1119
+ onUpgraded,
1120
+ onError
1376
1121
  }) {
1377
- const { customerId, apiKey } = useBillingPortal();
1378
1122
  const [isOpen, setIsOpen] = useState(false);
1379
1123
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1380
1124
  /* @__PURE__ */ jsx(
1381
1125
  "button",
1382
1126
  {
1127
+ type: "button",
1383
1128
  onClick: () => setIsOpen(true),
1384
1129
  className,
1385
1130
  style: {
@@ -1399,275 +1144,36 @@ function UpgradeButton({
1399
1144
  UpgradeModal,
1400
1145
  {
1401
1146
  isOpen,
1402
- targetPlanId,
1403
- customerId,
1404
- apiBaseUrl,
1405
- apiKey,
1406
- onConfirm: () => {
1147
+ planCode,
1148
+ returnUrl,
1149
+ preview,
1150
+ onStripeClientSecret,
1151
+ onCheckoutStarted: () => {
1407
1152
  setIsOpen(false);
1408
- onUpgraded?.();
1153
+ onCheckoutStarted?.();
1409
1154
  },
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}`
1155
+ onCompleted: () => {
1156
+ setIsOpen(false);
1157
+ onUpgraded?.();
1445
1158
  },
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
1159
+ onScheduled: () => {
1160
+ setIsOpen(false);
1161
+ onDowngradeScheduled?.();
1494
1162
  },
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
- ] })
1163
+ onError,
1164
+ onCancel: () => setIsOpen(false)
1654
1165
  }
1655
1166
  )
1656
1167
  ] });
1657
1168
  }
1658
1169
  function FeatureGate({
1659
- customerId,
1660
- feature,
1170
+ allowed,
1171
+ loading = false,
1661
1172
  fallback = null,
1662
1173
  children
1663
1174
  }) {
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
- }
1175
+ if (loading) return null;
1176
+ if (!allowed) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
1671
1177
  return /* @__PURE__ */ jsx(Fragment, { children });
1672
1178
  }
1673
1179
  function UsageGate({
@@ -1932,288 +1438,26 @@ function UsageAlert({
1932
1438
  f.key
1933
1439
  )) });
1934
1440
  }
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)"
1441
+ var TIER_STYLES = {
1442
+ free: {
1443
+ background: "var(--nozle-muted, var(--muted, #f1f5f9))",
1444
+ color: "var(--nozle-muted-foreground, var(--muted-foreground, #64748b))",
1445
+ border: "1px solid var(--nozle-border, var(--border, #e2e8f0))"
1982
1446
  },
1983
- deduct: {
1984
- background: "var(--nozle-destructive-bg, #fee2e2)",
1985
- color: "var(--nozle-destructive, #dc2626)"
1447
+ starter: {
1448
+ background: "color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 10%, transparent)",
1449
+ color: "var(--nozle-primary, var(--primary, #3b82f6))",
1450
+ border: "1px solid color-mix(in srgb, var(--nozle-primary, var(--primary, #3b82f6)) 30%, transparent)"
1986
1451
  },
1987
- purchase: {
1988
- background: "var(--nozle-info-bg, #dbeafe)",
1989
- color: "var(--nozle-info, #2563eb)"
1452
+ pro: {
1453
+ background: "var(--nozle-primary, var(--primary, #3b82f6))",
1454
+ color: "var(--nozle-primary-foreground, var(--primary-foreground, #ffffff))",
1455
+ border: "1px solid transparent"
1990
1456
  },
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"
1457
+ enterprise: {
1458
+ background: "var(--nozle-accent, var(--accent, #8b5cf6))",
1459
+ color: "var(--nozle-accent-foreground, var(--accent-foreground, #ffffff))",
1460
+ border: "1px solid transparent"
2217
1461
  }
2218
1462
  };
2219
1463
  function PlanBadge({ plan, tier = "free" }) {
@@ -2236,429 +1480,6 @@ function PlanBadge({ plan, tier = "free" }) {
2236
1480
  }
2237
1481
  );
2238
1482
  }
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
1483
  var BRAND_LABELS = {
2663
1484
  visa: "Visa",
2664
1485
  mastercard: "Mastercard",
@@ -2762,327 +1583,7 @@ function PaymentMethodDisplay({
2762
1583
  )
2763
1584
  ] });
2764
1585
  }
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
1586
 
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 };
1587
+ 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
1588
  //# sourceMappingURL=index.js.map
3088
1589
  //# sourceMappingURL=index.js.map