@nozle-js/react 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -9,9 +9,712 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
10
  var React__default = /*#__PURE__*/_interopDefault(React);
11
11
 
12
- // src/provider.tsx
12
+ // src/components/portal/BillingPortal.tsx
13
+
14
+ // src/components/portal/client.ts
15
+ var BillingPortalError = class extends Error {
16
+ constructor(code) {
17
+ super(
18
+ code === "changed" ? "Your subscription changed. Review its latest details before confirming again." : code === "unauthorized" ? "Your billing session has expired. Reconnect to continue." : code === "session" ? "Could not start your billing session. Please try again." : "Could not complete this billing request. Please try again."
19
+ );
20
+ this.code = code;
21
+ this.name = "BillingPortalError";
22
+ }
23
+ code;
24
+ };
25
+ function portalClient(session) {
26
+ let endpoint;
27
+ try {
28
+ endpoint = new URL(
29
+ `${(session.apiUrl ?? "https://api.nozle.app/core").replace(/\/$/, "")}/graphql`
30
+ );
31
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash || endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)) || !session.token?.trim() || /^(sk_|pk_)/.test(session.token) || /[\r\n]/.test(session.token)) {
32
+ throw new Error();
33
+ }
34
+ } catch {
35
+ throw new BillingPortalError("session");
36
+ }
37
+ return {
38
+ async request(query, variables = {}, signal) {
39
+ const controller = new AbortController();
40
+ const abort = () => controller.abort();
41
+ if (signal?.aborted) abort();
42
+ signal?.addEventListener("abort", abort, { once: true });
43
+ const timeout = setTimeout(abort, 2e4);
44
+ try {
45
+ const response = await fetch(endpoint.href, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "customer-portal-token": session.token
50
+ },
51
+ body: JSON.stringify({ query, variables }),
52
+ credentials: "omit",
53
+ cache: "no-store",
54
+ redirect: "error",
55
+ referrerPolicy: "no-referrer",
56
+ signal: controller.signal
57
+ });
58
+ if ([401, 403].includes(response.status))
59
+ throw new BillingPortalError("unauthorized");
60
+ if (!response.ok) throw new BillingPortalError("request");
61
+ const body = await response.json();
62
+ if (body.errors?.length) {
63
+ const unauthorized = body.errors.some(
64
+ (error) => error.extensions?.code === "unauthorized" || error.extensions?.status === "unauthorized"
65
+ );
66
+ throw new BillingPortalError(
67
+ unauthorized ? "unauthorized" : "request"
68
+ );
69
+ }
70
+ if (!body.data || typeof body.data !== "object")
71
+ throw new BillingPortalError("request");
72
+ return body.data;
73
+ } catch (error) {
74
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
75
+ throw error instanceof BillingPortalError ? error : new BillingPortalError("request");
76
+ } finally {
77
+ clearTimeout(timeout);
78
+ signal?.removeEventListener("abort", abort);
79
+ }
80
+ }
81
+ };
82
+ }
83
+ function invoiceDownloadUrl(value) {
84
+ try {
85
+ const url = new URL(value);
86
+ if (url.protocol !== "https:" || url.username || url.password)
87
+ throw new Error();
88
+ url.pathname = url.pathname.replace(
89
+ "/rails/active_storage/blobs/redirect/",
90
+ "/rails/active_storage/blobs/proxy/"
91
+ );
92
+ return url.href;
93
+ } catch {
94
+ throw new BillingPortalError("request");
95
+ }
96
+ }
97
+
98
+ // src/components/portal/operations.ts
99
+ var identityQuery = `query SdkPortalIdentity {
100
+ customerPortalUser { id name firstname lastname customerType email legalName
101
+ taxIdentificationNumber currency applicableTimezone
102
+ addressLine1 addressLine2 city state zipcode country
103
+ shippingAddress { addressLine1 addressLine2 city state zipcode country }
104
+ }
105
+ customerPortalOrganization { id name }
106
+ }`;
107
+ var subscriptionsQuery = `query SdkPortalSubscriptions($page: Int) {
108
+ customerPortalSubscriptions(status: [active], limit: 20, page: $page) {
109
+ metadata { currentPage totalPages }
110
+ collection { id externalId status endingAt name currentBillingPeriodEndingAt
111
+ nextSubscription { id status plan { name } }
112
+ plan { id name invoiceDisplayName code amountCents amountCurrency interval }
113
+ }
114
+ }
115
+ }`;
116
+ var subscriptionQuery = `query SdkPortalSubscription($id: ID!) {
117
+ customerPortalSubscription(id: $id) {
118
+ id externalId status endingAt name currentBillingPeriodEndingAt
119
+ nextSubscription { id status plan { name } }
120
+ plan { id name invoiceDisplayName code amountCents amountCurrency interval }
121
+ }
122
+ }`;
123
+ var usageQuery = `query SdkPortalUsage($subscriptionId: ID!) {
124
+ customerPortalCustomerUsage(subscriptionId: $subscriptionId) {
125
+ amountCents currency fromDatetime toDatetime
126
+ chargesUsage { id units amountCents charge { id invoiceDisplayName } feature { id code name } }
127
+ }
128
+ }`;
129
+ var invoicesQuery = `query SdkPortalInvoices($page: Int, $searchTerm: String) {
130
+ customerPortalInvoices(status: [finalized], limit: 8, page: $page, searchTerm: $searchTerm) {
131
+ metadata { currentPage totalPages totalCount }
132
+ collection { id number issuingDate totalAmountCents totalDueAmountCents currency
133
+ paymentStatus paymentOverdue paymentDisputeLostAt }
134
+ }
135
+ }`;
136
+ var walletsQuery = `query SdkPortalWallets($page: Int) {
137
+ customerPortalWallets(status: active, limit: 10, page: $page) {
138
+ metadata { currentPage totalPages }
139
+ collection { id name currency balanceCents creditsBalance consumedCredits expirationAt rateAmount
140
+ paidTopUpMinAmountCents paidTopUpMaxAmountCents }
141
+ }
142
+ }`;
143
+ var updateCustomerMutation = `mutation SdkPortalUpdateCustomer($input: UpdateCustomerPortalCustomerInput!) {
144
+ updateCustomerPortalCustomer(input: $input) { id }
145
+ }`;
146
+ var downloadInvoiceMutation = `mutation SdkPortalDownloadInvoice($input: DownloadCustomerPortalInvoiceInput!) {
147
+ downloadCustomerPortalInvoice(input: $input) { id fileUrl }
148
+ }`;
149
+ var topUpMutation = `mutation SdkPortalTopUp($input: CreateCustomerPortalWalletTransactionInput!) {
150
+ createCustomerPortalWalletTransaction(input: $input) { collection { id } }
151
+ }`;
152
+
153
+ // src/components/portal/styles.ts
154
+ var portalStyles = `
155
+ .nozle-billing-portal{color:var(--nozle-portal-text,#202922);background:var(--nozle-portal-background,#fff);font:14px/1.55 system-ui,sans-serif;box-sizing:border-box;width:100%;min-width:0;padding:clamp(18px,4vw,40px);border-radius:12px}
156
+ .nozle-billing-portal *{box-sizing:border-box}
157
+ .nozle-billing-portal h2,.nozle-billing-portal h3,.nozle-billing-portal p{margin:0}
158
+ .nozle-billing-portal h2{font-size:24px;letter-spacing:-.6px;line-height:1.3}
159
+ .nozle-billing-portal h3{font-size:16px;line-height:1.4}
160
+ .nozle-billing-portal header{margin-bottom:30px}
161
+ .nozle-billing-portal section{margin-top:28px;padding-top:24px;border-top:1px solid var(--nozle-portal-border,#e6e9e5)}
162
+ .nozle-billing-portal button,.nozle-billing-portal .nzp-link{appearance:none;display:inline-flex;align-items:center;justify-content:center;font:inherit;font-weight:550;line-height:1.4;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:var(--nozle-portal-text,#202922);padding:9px 14px;cursor:pointer;text-decoration:none;white-space:normal}
163
+ .nozle-billing-portal button:hover,.nozle-billing-portal .nzp-link:hover{background:var(--nozle-portal-muted-background,#f4f6f2)}
164
+ .nozle-billing-portal button:disabled{opacity:.55;cursor:wait}
165
+ .nozle-billing-portal :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
166
+ .nozle-billing-portal .nzp-primary{background:var(--nozle-portal-accent,#526949);border-color:var(--nozle-portal-accent,#526949);color:#fff}
167
+ .nozle-billing-portal .nzp-primary:hover{filter:brightness(.92);background:var(--nozle-portal-accent,#526949)}
168
+ .nozle-billing-portal .nzp-muted{color:var(--nozle-portal-muted,#687166);font-size:13px}
169
+ .nozle-billing-portal .nzp-row{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
170
+ .nozle-billing-portal .nzp-stack{display:grid;gap:12px;margin-top:16px}
171
+ .nozle-billing-portal .nzp-card{border:1px solid var(--nozle-portal-border,#e0e6dc);border-radius:10px;padding:20px;min-width:0}
172
+ .nozle-billing-portal .nzp-price{font-size:26px;letter-spacing:-.5px;margin:12px 0 4px}
173
+ .nozle-billing-portal .nzp-badge{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.3px;border-radius:5px;background:var(--nozle-portal-muted-background,#edf3e8);padding:3px 7px}
174
+ .nozle-billing-portal .nzp-empty{border:1px dashed var(--nozle-portal-border,#dce2d9);border-radius:8px;padding:22px;text-align:center;color:var(--nozle-portal-muted,#687166);margin-top:16px}
175
+ .nozle-billing-portal .nzp-error{color:#922e25;background:#fff4f2;border:1px solid #f2d2ce;border-radius:8px;padding:14px;margin:16px 0;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
176
+ .nozle-billing-portal .nzp-table{overflow-x:auto;margin-top:16px}
177
+ .nozle-billing-portal table{width:100%;border-collapse:collapse;text-align:left;font-size:13px}
178
+ .nozle-billing-portal th{font-weight:500;color:var(--nozle-portal-muted,#687166);background:var(--nozle-portal-muted-background,#f7f8f5)}
179
+ .nozle-billing-portal th,.nozle-billing-portal td{padding:12px 10px;border-bottom:1px solid var(--nozle-portal-border,#e6e9e5);white-space:nowrap}
180
+ .nozle-billing-portal label{display:grid;gap:6px;font-size:13px;font-weight:500;min-width:0}
181
+ .nozle-billing-portal input,.nozle-billing-portal select{width:100%;min-width:0;font:inherit;color:inherit;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:6px;padding:9px 10px}
182
+ .nozle-billing-portal .nzp-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;margin:20px 0}
183
+ .nozle-billing-portal .nzp-actions{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-top:20px}
184
+ .nozle-billing-portal dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr));gap:18px;margin:16px 0 0}
185
+ .nozle-billing-portal dt{font-size:12px;color:var(--nozle-portal-muted,#687166)}
186
+ .nozle-billing-portal dd{margin:3px 0 0;overflow-wrap:anywhere}
187
+ .nozle-billing-portal footer{margin-top:30px;text-align:center;color:var(--nozle-portal-muted,#687166);font-size:12px}
188
+ `;
189
+
190
+ // src/components/portal/timezones.ts
191
+ var portalTimezones = {
192
+ TZ_AFRICA_ALGIERS: "Africa/Algiers",
193
+ TZ_AFRICA_CAIRO: "Africa/Cairo",
194
+ TZ_AFRICA_CASABLANCA: "Africa/Casablanca",
195
+ TZ_AFRICA_HARARE: "Africa/Harare",
196
+ TZ_AFRICA_JOHANNESBURG: "Africa/Johannesburg",
197
+ TZ_AFRICA_MONROVIA: "Africa/Monrovia",
198
+ TZ_AFRICA_NAIROBI: "Africa/Nairobi",
199
+ TZ_AMERICA_ARGENTINA_BUENOS_AIRES: "America/Argentina/Buenos_Aires",
200
+ TZ_AMERICA_BOGOTA: "America/Bogota",
201
+ TZ_AMERICA_CARACAS: "America/Caracas",
202
+ TZ_AMERICA_CHICAGO: "America/Chicago",
203
+ TZ_AMERICA_CHIHUAHUA: "America/Chihuahua",
204
+ TZ_AMERICA_DENVER: "America/Denver",
205
+ TZ_AMERICA_NUUK: "America/Nuuk",
206
+ TZ_AMERICA_GUATEMALA: "America/Guatemala",
207
+ TZ_AMERICA_GUYANA: "America/Guyana",
208
+ TZ_AMERICA_HALIFAX: "America/Halifax",
209
+ TZ_AMERICA_INDIANA_INDIANAPOLIS: "America/Indiana/Indianapolis",
210
+ TZ_AMERICA_JUNEAU: "America/Juneau",
211
+ TZ_AMERICA_LA_PAZ: "America/La_Paz",
212
+ TZ_AMERICA_LIMA: "America/Lima",
213
+ TZ_AMERICA_LOS_ANGELES: "America/Los_Angeles",
214
+ TZ_AMERICA_MAZATLAN: "America/Mazatlan",
215
+ TZ_AMERICA_MEXICO_CITY: "America/Mexico_City",
216
+ TZ_AMERICA_MONTERREY: "America/Monterrey",
217
+ TZ_AMERICA_MONTEVIDEO: "America/Montevideo",
218
+ TZ_AMERICA_NEW_YORK: "America/New_York",
219
+ TZ_AMERICA_PHOENIX: "America/Phoenix",
220
+ TZ_AMERICA_PUERTO_RICO: "America/Puerto_Rico",
221
+ TZ_AMERICA_REGINA: "America/Regina",
222
+ TZ_AMERICA_SANTIAGO: "America/Santiago",
223
+ TZ_AMERICA_SAO_PAULO: "America/Sao_Paulo",
224
+ TZ_AMERICA_ST_JOHNS: "America/St_Johns",
225
+ TZ_AMERICA_TIJUANA: "America/Tijuana",
226
+ TZ_ASIA_ALMATY: "Asia/Almaty",
227
+ TZ_ASIA_BAGHDAD: "Asia/Baghdad",
228
+ TZ_ASIA_BAKU: "Asia/Baku",
229
+ TZ_ASIA_BANGKOK: "Asia/Bangkok",
230
+ TZ_ASIA_CHONGQING: "Asia/Chongqing",
231
+ TZ_ASIA_COLOMBO: "Asia/Colombo",
232
+ TZ_ASIA_DHAKA: "Asia/Dhaka",
233
+ TZ_ASIA_HONG_KONG: "Asia/Hong_Kong",
234
+ TZ_ASIA_IRKUTSK: "Asia/Irkutsk",
235
+ TZ_ASIA_JAKARTA: "Asia/Jakarta",
236
+ TZ_ASIA_JERUSALEM: "Asia/Jerusalem",
237
+ TZ_ASIA_KABUL: "Asia/Kabul",
238
+ TZ_ASIA_KAMCHATKA: "Asia/Kamchatka",
239
+ TZ_ASIA_KARACHI: "Asia/Karachi",
240
+ TZ_ASIA_KATHMANDU: "Asia/Kathmandu",
241
+ TZ_ASIA_KOLKATA: "Asia/Kolkata",
242
+ TZ_ASIA_KRASNOYARSK: "Asia/Krasnoyarsk",
243
+ TZ_ASIA_KUALA_LUMPUR: "Asia/Kuala_Lumpur",
244
+ TZ_ASIA_KUWAIT: "Asia/Kuwait",
245
+ TZ_ASIA_MAGADAN: "Asia/Magadan",
246
+ TZ_ASIA_MUSCAT: "Asia/Muscat",
247
+ TZ_ASIA_NOVOSIBIRSK: "Asia/Novosibirsk",
248
+ TZ_ASIA_YANGON: "Asia/Yangon",
249
+ TZ_ASIA_RIYADH: "Asia/Riyadh",
250
+ TZ_ASIA_SEOUL: "Asia/Seoul",
251
+ TZ_ASIA_SHANGHAI: "Asia/Shanghai",
252
+ TZ_ASIA_SINGAPORE: "Asia/Singapore",
253
+ TZ_ASIA_SREDNEKOLYMSK: "Asia/Srednekolymsk",
254
+ TZ_ASIA_TAIPEI: "Asia/Taipei",
255
+ TZ_ASIA_TASHKENT: "Asia/Tashkent",
256
+ TZ_ASIA_TBILISI: "Asia/Tbilisi",
257
+ TZ_ASIA_TEHRAN: "Asia/Tehran",
258
+ TZ_ASIA_TOKYO: "Asia/Tokyo",
259
+ TZ_ASIA_ULAANBAATAR: "Asia/Ulaanbaatar",
260
+ TZ_ASIA_URUMQI: "Asia/Urumqi",
261
+ TZ_ASIA_VLADIVOSTOK: "Asia/Vladivostok",
262
+ TZ_ASIA_YAKUTSK: "Asia/Yakutsk",
263
+ TZ_ASIA_YEKATERINBURG: "Asia/Yekaterinburg",
264
+ TZ_ASIA_YEREVAN: "Asia/Yerevan",
265
+ TZ_ATLANTIC_AZORES: "Atlantic/Azores",
266
+ TZ_ATLANTIC_CAPE_VERDE: "Atlantic/Cape_Verde",
267
+ TZ_ATLANTIC_SOUTH_GEORGIA: "Atlantic/South_Georgia",
268
+ TZ_AUSTRALIA_ADELAIDE: "Australia/Adelaide",
269
+ TZ_AUSTRALIA_BRISBANE: "Australia/Brisbane",
270
+ TZ_AUSTRALIA_CANBERRA: "Australia/Canberra",
271
+ TZ_AUSTRALIA_DARWIN: "Australia/Darwin",
272
+ TZ_AUSTRALIA_HOBART: "Australia/Hobart",
273
+ TZ_AUSTRALIA_MELBOURNE: "Australia/Melbourne",
274
+ TZ_AUSTRALIA_PERTH: "Australia/Perth",
275
+ TZ_AUSTRALIA_SYDNEY: "Australia/Sydney",
276
+ TZ_EUROPE_AMSTERDAM: "Europe/Amsterdam",
277
+ TZ_EUROPE_ATHENS: "Europe/Athens",
278
+ TZ_EUROPE_BELGRADE: "Europe/Belgrade",
279
+ TZ_EUROPE_BERLIN: "Europe/Berlin",
280
+ TZ_EUROPE_BRATISLAVA: "Europe/Bratislava",
281
+ TZ_EUROPE_BRUSSELS: "Europe/Brussels",
282
+ TZ_EUROPE_BUCHAREST: "Europe/Bucharest",
283
+ TZ_EUROPE_BUDAPEST: "Europe/Budapest",
284
+ TZ_EUROPE_COPENHAGEN: "Europe/Copenhagen",
285
+ TZ_EUROPE_DUBLIN: "Europe/Dublin",
286
+ TZ_EUROPE_HELSINKI: "Europe/Helsinki",
287
+ TZ_EUROPE_ISTANBUL: "Europe/Istanbul",
288
+ TZ_EUROPE_KALININGRAD: "Europe/Kaliningrad",
289
+ TZ_EUROPE_KYIV: "Europe/Kyiv",
290
+ TZ_EUROPE_LISBON: "Europe/Lisbon",
291
+ TZ_EUROPE_LJUBLJANA: "Europe/Ljubljana",
292
+ TZ_EUROPE_LONDON: "Europe/London",
293
+ TZ_EUROPE_MADRID: "Europe/Madrid",
294
+ TZ_EUROPE_MINSK: "Europe/Minsk",
295
+ TZ_EUROPE_MOSCOW: "Europe/Moscow",
296
+ TZ_EUROPE_PARIS: "Europe/Paris",
297
+ TZ_EUROPE_PRAGUE: "Europe/Prague",
298
+ TZ_EUROPE_RIGA: "Europe/Riga",
299
+ TZ_EUROPE_ROME: "Europe/Rome",
300
+ TZ_EUROPE_SAMARA: "Europe/Samara",
301
+ TZ_EUROPE_SARAJEVO: "Europe/Sarajevo",
302
+ TZ_EUROPE_SKOPJE: "Europe/Skopje",
303
+ TZ_EUROPE_SOFIA: "Europe/Sofia",
304
+ TZ_EUROPE_STOCKHOLM: "Europe/Stockholm",
305
+ TZ_EUROPE_TALLINN: "Europe/Tallinn",
306
+ TZ_EUROPE_VIENNA: "Europe/Vienna",
307
+ TZ_EUROPE_VILNIUS: "Europe/Vilnius",
308
+ TZ_EUROPE_VOLGOGRAD: "Europe/Volgograd",
309
+ TZ_EUROPE_WARSAW: "Europe/Warsaw",
310
+ TZ_EUROPE_ZAGREB: "Europe/Zagreb",
311
+ TZ_EUROPE_ZURICH: "Europe/Zurich",
312
+ TZ_ETC_GMT_12: "Etc/GMT+12",
313
+ TZ_PACIFIC_APIA: "Pacific/Apia",
314
+ TZ_PACIFIC_AUCKLAND: "Pacific/Auckland",
315
+ TZ_PACIFIC_CHATHAM: "Pacific/Chatham",
316
+ TZ_PACIFIC_FAKAOFO: "Pacific/Fakaofo",
317
+ TZ_PACIFIC_FIJI: "Pacific/Fiji",
318
+ TZ_PACIFIC_GUADALCANAL: "Pacific/Guadalcanal",
319
+ TZ_PACIFIC_GUAM: "Pacific/Guam",
320
+ TZ_PACIFIC_HONOLULU: "Pacific/Honolulu",
321
+ TZ_PACIFIC_MAJURO: "Pacific/Majuro",
322
+ TZ_PACIFIC_MIDWAY: "Pacific/Midway",
323
+ TZ_PACIFIC_NOUMEA: "Pacific/Noumea",
324
+ TZ_PACIFIC_PAGO_PAGO: "Pacific/Pago_Pago",
325
+ TZ_PACIFIC_PORT_MORESBY: "Pacific/Port_Moresby",
326
+ TZ_PACIFIC_TONGATAPU: "Pacific/Tongatapu",
327
+ TZ_UTC: "UTC"
328
+ };
329
+
330
+ // src/core/decimal.ts
331
+ function parseDecimal(value) {
332
+ const match = value.trim().match(/^([+-]?)(\d+)(?:\.(\d+))?$/);
333
+ if (!match) return null;
334
+ const rawInteger = match[2] ?? "0";
335
+ let firstIntegerDigit = 0;
336
+ while (firstIntegerDigit < rawInteger.length - 1 && rawInteger[firstIntegerDigit] === "0") {
337
+ firstIntegerDigit += 1;
338
+ }
339
+ const integer = rawInteger.slice(firstIntegerDigit);
340
+ const rawFraction = match[3] ?? "";
341
+ let fractionEnd = rawFraction.length;
342
+ while (fractionEnd > 0 && rawFraction[fractionEnd - 1] === "0") {
343
+ fractionEnd -= 1;
344
+ }
345
+ const fraction = rawFraction.slice(0, fractionEnd);
346
+ const zero = integer === "0" && fraction === "";
347
+ return {
348
+ sign: !zero && match[1] === "-" ? -1 : 1,
349
+ integer,
350
+ fraction
351
+ };
352
+ }
353
+ function compareDecimalStrings(left, right) {
354
+ const a = parseDecimal(left);
355
+ const b = parseDecimal(right);
356
+ if (!a || !b) throw new Error("invalid decimal value");
357
+ if (a.sign !== b.sign) return a.sign > b.sign ? 1 : -1;
358
+ let magnitude = 0;
359
+ if (a.integer.length !== b.integer.length) {
360
+ magnitude = a.integer.length > b.integer.length ? 1 : -1;
361
+ } else if (a.integer !== b.integer) {
362
+ magnitude = a.integer > b.integer ? 1 : -1;
363
+ } else {
364
+ const width = Math.max(a.fraction.length, b.fraction.length);
365
+ const aFraction = a.fraction.padEnd(width, "0");
366
+ const bFraction = b.fraction.padEnd(width, "0");
367
+ if (aFraction !== bFraction) magnitude = aFraction > bFraction ? 1 : -1;
368
+ }
369
+ return magnitude * a.sign;
370
+ }
371
+
372
+ // src/components/portal/amounts.ts
373
+ function walletAmountCents(credits, rate, currencyDigits) {
374
+ if (!/^\d{1,30}(\.\d{1,5})?$/.test(credits) || !Number.isFinite(rate) || rate <= 0)
375
+ return null;
376
+ const decimal = (value) => {
377
+ const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(value);
378
+ return {
379
+ value: BigInt(match[1] + (match[2] ?? "")),
380
+ scale: (match[2]?.length ?? 0) - Number(match[3] ?? 0)
381
+ };
382
+ };
383
+ const left = decimal(credits), right = decimal(String(rate));
384
+ const coefficient = left.value * right.value;
385
+ const scale = left.scale + right.scale - currencyDigits;
386
+ if (scale <= 0) return String(coefficient * 10n ** BigInt(-scale));
387
+ const padded = String(coefficient).padStart(scale + 1, "0");
388
+ return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
389
+ }
390
+ var styles = `
391
+ .nozle-cancellation{font:inherit;color:var(--nozle-portal-text,#202922)}
392
+ .nozle-cancellation button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
393
+ .nozle-cancellation button:disabled{opacity:.55;cursor:wait}
394
+ .nozle-cancellation :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
395
+ .nozle-cancellation .nzc-backdrop{position:fixed;inset:0;background:#0006;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}
396
+ .nozle-cancellation .nzc-dialog{box-sizing:border-box;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:12px;padding:24px;width:100%;max-width:440px;max-height:90dvh;overflow:auto;box-shadow:0 12px 48px #0003}
397
+ .nozle-cancellation .nzc-dialog h3{margin:0 0 16px;font-size:20px}
398
+ .nozle-cancellation .nzc-dialog p{margin:12px 0;line-height:1.55}
399
+ .nozle-cancellation .nzc-buttons{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;margin-top:20px}
400
+ .nozle-cancellation [role=alert]{color:#922e25}
401
+ `;
402
+ function formatDate(value, locale, timezone) {
403
+ const timeZone = timezone.startsWith("TZ_") ? portalTimezones[timezone] ?? "UTC" : timezone;
404
+ return new Intl.DateTimeFormat(locale, {
405
+ dateStyle: "long",
406
+ timeZone
407
+ }).format(new Date(value));
408
+ }
409
+ function completed(operation, state) {
410
+ if (!state) return false;
411
+ return operation === "cancel" ? Boolean(state.endingAt) || ["terminated", "canceled"].includes(state.status) : state.status === "active" && !state.endingAt;
412
+ }
413
+ function CancellationControl(props) {
414
+ return /* @__PURE__ */ jsxRuntime.jsx(
415
+ CancellationFlow,
416
+ {
417
+ ...props
418
+ },
419
+ `${props.subscription.id}:${props.subscription.externalId}`
420
+ );
421
+ }
422
+ function CancellationFlow({
423
+ subscription,
424
+ actions,
425
+ refresh,
426
+ onChanged,
427
+ onError,
428
+ locale = "en-US",
429
+ timezone = "UTC",
430
+ nonce
431
+ }) {
432
+ const [open, setOpen] = React.useState(false);
433
+ const [busy, setBusy] = React.useState(false);
434
+ const [preview, setPreview] = React.useState();
435
+ const [failure, setFailure] = React.useState();
436
+ const [notice, setNotice] = React.useState("");
437
+ const [observed, setObserved] = React.useState(
438
+ subscription
439
+ );
440
+ const request = React.useRef(void 0);
441
+ const inFlight = React.useRef(null);
442
+ const trigger = React.useRef(null);
443
+ const dialog = React.useRef(null);
444
+ const titleId = React.useId();
445
+ const descriptionId = React.useId();
446
+ const active = observed?.status === "active";
447
+ const ending = observed?.endingAt;
448
+ const expired = Boolean(ending && Date.parse(ending) <= Date.now());
449
+ const operation = ending ? "uncancel" : "cancel";
450
+ React.useEffect(
451
+ () => setObserved(subscription),
452
+ [
453
+ subscription.id,
454
+ subscription.externalId,
455
+ subscription.status,
456
+ subscription.endingAt,
457
+ subscription.name,
458
+ subscription.pendingPlanName
459
+ ]
460
+ );
461
+ React.useEffect(() => () => inFlight.current?.abort(), []);
462
+ React.useEffect(() => {
463
+ if (!open) return;
464
+ dialog.current?.focus();
465
+ return () => trigger.current?.focus();
466
+ }, [open]);
467
+ function report(cause, message) {
468
+ const error = cause instanceof BillingPortalError ? cause : new BillingPortalError("request");
469
+ setFailure(error.code === "unauthorized" ? error.message : message);
470
+ onError?.(error);
471
+ }
472
+ async function run(task) {
473
+ if (inFlight.current) return;
474
+ const controller = new AbortController();
475
+ inFlight.current = controller;
476
+ setBusy(true);
477
+ let timedOut = false;
478
+ let rejectAborted = () => {
479
+ };
480
+ const aborted = new Promise((_resolve, reject) => {
481
+ rejectAborted = () => reject(new DOMException("Aborted", "AbortError"));
482
+ controller.signal.addEventListener("abort", rejectAborted, {
483
+ once: true
484
+ });
485
+ });
486
+ const timeout = setTimeout(() => {
487
+ timedOut = true;
488
+ controller.abort();
489
+ }, 3e4);
490
+ try {
491
+ await Promise.race([task(controller.signal), aborted]);
492
+ } catch (cause) {
493
+ if (timedOut)
494
+ report(
495
+ cause,
496
+ "This request timed out. Check its status before trying again."
497
+ );
498
+ } finally {
499
+ clearTimeout(timeout);
500
+ controller.signal.removeEventListener("abort", rejectAborted);
501
+ if (!controller.signal.aborted || timedOut) setBusy(false);
502
+ if (inFlight.current === controller) inFlight.current = null;
503
+ }
504
+ }
505
+ function finish(state, op) {
506
+ setObserved(state);
507
+ request.current = void 0;
508
+ setOpen(false);
509
+ setPreview(void 0);
510
+ setFailure(void 0);
511
+ setNotice(
512
+ op === "cancel" ? "Cancellation scheduled." : "Your subscription will continue to renew."
513
+ );
514
+ onChanged?.();
515
+ }
516
+ async function loadPreview() {
517
+ setOpen(true);
518
+ setFailure(void 0);
519
+ if (request.current) return;
520
+ setPreview(void 0);
521
+ await run(async (signal) => {
522
+ try {
523
+ const state = await refresh(signal);
524
+ if (signal.aborted) return;
525
+ setObserved(state);
526
+ if (!state || state.status !== "active" || Boolean(state.endingAt) !== Boolean(ending)) {
527
+ setFailure(
528
+ "Your subscription changed. Close this dialog and refresh your billing details."
529
+ );
530
+ return;
531
+ }
532
+ const result = await actions.preview({
533
+ subscriptionId: subscription.externalId,
534
+ operation,
535
+ signal
536
+ });
537
+ if (signal.aborted) return;
538
+ if (result.operation !== operation || !Number.isFinite(Date.parse(result.effectiveAt)) || result.renewalAt !== null && !Number.isFinite(Date.parse(result.renewalAt)))
539
+ throw new Error();
540
+ setPreview(result);
541
+ } catch (cause) {
542
+ if (!signal.aborted)
543
+ report(cause, "Could not load this change. Please try again.");
544
+ }
545
+ });
546
+ }
547
+ async function confirm() {
548
+ if (!preview || inFlight.current) return;
549
+ if (!request.current) {
550
+ request.current = {
551
+ operation: preview.operation,
552
+ idempotencyKey: crypto.randomUUID(),
553
+ ...preview.operation === "cancel" ? { expectedEffectiveAt: preview.effectiveAt } : {},
554
+ attempted: false,
555
+ accepted: false
556
+ };
557
+ }
558
+ setFailure(void 0);
559
+ await run(async (signal) => {
560
+ const current = request.current;
561
+ let cause;
562
+ try {
563
+ if (current.attempted) {
564
+ const state = await refresh(signal);
565
+ if (signal.aborted) return;
566
+ if (completed(current.operation, state)) {
567
+ finish(state, current.operation);
568
+ return;
569
+ }
570
+ }
571
+ if (!current.accepted) {
572
+ current.attempted = true;
573
+ await actions.apply({
574
+ subscriptionId: subscription.externalId,
575
+ operation: current.operation,
576
+ idempotencyKey: current.idempotencyKey,
577
+ expectedEffectiveAt: current.expectedEffectiveAt,
578
+ signal
579
+ });
580
+ current.accepted = true;
581
+ }
582
+ } catch (error) {
583
+ cause = error;
584
+ if (!signal.aborted && error instanceof BillingPortalError && error.code === "changed") {
585
+ request.current = void 0;
586
+ setPreview(void 0);
587
+ try {
588
+ const state = await refresh(signal);
589
+ if (signal.aborted) return;
590
+ setObserved(state);
591
+ } catch {
592
+ if (signal.aborted) return;
593
+ }
594
+ setFailure(error.message);
595
+ onError?.(error);
596
+ return;
597
+ }
598
+ }
599
+ if (signal.aborted) return;
600
+ try {
601
+ const state = await refresh(signal);
602
+ if (signal.aborted) return;
603
+ setObserved(state);
604
+ if (completed(current.operation, state)) {
605
+ finish(state, current.operation);
606
+ return;
607
+ }
608
+ } catch (error) {
609
+ cause ??= error;
610
+ }
611
+ if (!signal.aborted)
612
+ report(
613
+ cause,
614
+ "We could not confirm this change. Check its status before trying again."
615
+ );
616
+ });
617
+ }
618
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nozle-cancellation", children: [
619
+ /* @__PURE__ */ jsxRuntime.jsx("style", { nonce, children: styles }),
620
+ notice && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: notice }),
621
+ active && !expired && /* @__PURE__ */ jsxRuntime.jsx("button", { ref: trigger, type: "button", onClick: () => void loadPreview(), children: ending ? "Keep my subscription" : "Cancel subscription" }),
622
+ open && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzc-backdrop", children: /* @__PURE__ */ jsxRuntime.jsxs(
623
+ "div",
624
+ {
625
+ className: "nzc-dialog",
626
+ ref: dialog,
627
+ role: "dialog",
628
+ "aria-modal": "true",
629
+ "aria-labelledby": titleId,
630
+ "aria-describedby": descriptionId,
631
+ tabIndex: -1,
632
+ onKeyDown: (event) => {
633
+ if (event.key === "Escape" && !busy) {
634
+ event.preventDefault();
635
+ setOpen(false);
636
+ }
637
+ if (event.key === "Tab") {
638
+ const buttons = Array.from(
639
+ dialog.current?.querySelectorAll(
640
+ "button:not(:disabled)"
641
+ ) ?? []
642
+ );
643
+ const first = buttons[0], last = buttons[buttons.length - 1];
644
+ if (!first) {
645
+ event.preventDefault();
646
+ return;
647
+ }
648
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
649
+ event.preventDefault();
650
+ last.focus();
651
+ } else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog.current)) {
652
+ event.preventDefault();
653
+ first.focus();
654
+ }
655
+ }
656
+ },
657
+ children: [
658
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { id: titleId, children: (preview?.operation ?? operation) === "cancel" ? "Cancel subscription?" : "Keep your subscription?" }),
659
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: descriptionId, children: subscription.name }),
660
+ preview && (preview.operation === "cancel" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
661
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
662
+ "If you confirm, your subscription will end on",
663
+ " ",
664
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: formatDate(preview.effectiveAt, locale, timezone) }),
665
+ "."
666
+ ] }),
667
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "You can keep your subscription before cancellation takes effect. Outstanding invoices and usage charges still apply." }),
668
+ observed?.pendingPlanName && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
669
+ "Your scheduled change to ",
670
+ observed.pendingPlanName,
671
+ " will be removed. Keeping your subscription later will not restore that change."
672
+ ] })
673
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
674
+ "Your current plan will continue to renew",
675
+ preview.renewalAt ? ` on ${formatDate(preview.renewalAt, locale, timezone)}` : "",
676
+ "."
677
+ ] })),
678
+ busy && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: request.current ? "Confirming subscription status\u2026" : "Loading subscription details\u2026" }),
679
+ failure && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: failure }),
680
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzc-buttons", children: [
681
+ /* @__PURE__ */ jsxRuntime.jsx(
682
+ "button",
683
+ {
684
+ type: "button",
685
+ disabled: busy,
686
+ onClick: () => setOpen(false),
687
+ children: "Close"
688
+ }
689
+ ),
690
+ !preview && failure && /* @__PURE__ */ jsxRuntime.jsx(
691
+ "button",
692
+ {
693
+ type: "button",
694
+ disabled: busy,
695
+ onClick: () => void loadPreview(),
696
+ children: "Try again"
697
+ }
698
+ ),
699
+ preview && /* @__PURE__ */ jsxRuntime.jsx(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ disabled: busy,
704
+ onClick: () => void confirm(),
705
+ children: request.current ? "Check status / retry" : preview.operation === "cancel" ? "Confirm cancellation" : "Confirm keep subscription"
706
+ }
707
+ )
708
+ ] })
709
+ ]
710
+ }
711
+ ) })
712
+ ] });
713
+ }
13
714
  function createClient(publishableKey, baseUrl) {
14
- const base = baseUrl.replace(/\/+$/, "");
715
+ let end = baseUrl.length;
716
+ while (end > 0 && baseUrl[end - 1] === "/") end--;
717
+ const base = baseUrl.slice(0, end);
15
718
  return {
16
719
  publishableKey,
17
720
  baseUrl: base,
@@ -68,82 +771,6 @@ function useBillingContext() {
68
771
  function useNozleClient() {
69
772
  return useBillingContext().client;
70
773
  }
71
- function usePlans() {
72
- const { client } = useBillingContext();
73
- const [plans, setPlans] = React.useState([]);
74
- const [isLoading, setIsLoading] = React.useState(true);
75
- React.useEffect(() => {
76
- let cancelled = false;
77
- if (!client) {
78
- setIsLoading(false);
79
- return;
80
- }
81
- void client.catalogFetch("/api/v1/plans").then(async (response) => {
82
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
83
- const data = await response.json();
84
- if (!cancelled) setPlans(data.plans ?? []);
85
- }).finally(() => {
86
- if (!cancelled) setIsLoading(false);
87
- });
88
- return () => {
89
- cancelled = true;
90
- };
91
- }, [client]);
92
- return { plans, isLoading };
93
- }
94
-
95
- // src/components/billing/checkout-navigation.ts
96
- function navigateToCheckout(url, location = window.location) {
97
- location.assign(url);
98
- }
99
-
100
- // src/hooks/use-checkout.ts
101
- function useCheckout() {
102
- const { createCheckout } = useBillingContext();
103
- const [isLoading, setIsLoading] = React.useState(false);
104
- const [error, setError] = React.useState(null);
105
- const [checkout, setCheckout] = React.useState(null);
106
- const fetchClientSecret = React.useCallback(
107
- async (planCode, returnUrl) => {
108
- setIsLoading(true);
109
- setError(null);
110
- try {
111
- if (!createCheckout) {
112
- throw new Error("BillingProvider createCheckout callback is required for checkout");
113
- }
114
- const result = await createCheckout({
115
- planCode,
116
- returnUrl: returnUrl ?? window.location.href
117
- });
118
- setCheckout(result);
119
- if ("client_secret" in result || "clientSecret" in result) {
120
- const clientSecret = result.client_secret ?? result.clientSecret;
121
- if (clientSecret) return clientSecret;
122
- }
123
- if ("url" in result && result.url) {
124
- navigateToCheckout(result.url);
125
- return null;
126
- }
127
- if ("type" in result && result.type === "hosted") {
128
- navigateToCheckout(result.payment_url);
129
- return null;
130
- }
131
- if ("type" in result && ["completed", "scheduled", "razorpay", "processing"].includes(result.type)) {
132
- return null;
133
- }
134
- throw new Error("Checkout response did not include a supported result type");
135
- } catch (cause) {
136
- const checkoutError = cause instanceof Error ? cause : new Error(String(cause));
137
- setError(checkoutError);
138
- throw checkoutError;
139
- } finally {
140
- setIsLoading(false);
141
- }
142
- },
143
- [createCheckout]
144
- );
145
- return { fetchClientSecret, checkout, isLoading, error };
146
- }
147
774
 
148
775
  // src/components/billing/razorpay-checkout.ts
149
776
  var loadingScript;
@@ -289,6 +916,21 @@ async function resumeCheckout(result, actions) {
289
916
  actions.onProcessing?.(status);
290
917
  return void 0;
291
918
  }
919
+
920
+ // src/components/billing/checkout-navigation.ts
921
+ function navigateToCheckout(url, location = window.location) {
922
+ const parsed = new URL(
923
+ url,
924
+ typeof window !== "undefined" ? window.location.href : void 0
925
+ );
926
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
927
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
928
+ throw new Error("Checkout URL must use HTTPS");
929
+ }
930
+ if (parsed.username || parsed.password)
931
+ throw new Error("Checkout URL must not contain credentials");
932
+ location.assign(url);
933
+ }
292
934
  async function handleCheckoutResult(result, options) {
293
935
  if ("type" in result && result.type === "scheduled") {
294
936
  options.onScheduled?.(result);
@@ -308,11 +950,11 @@ async function handleCheckoutResult(result, options) {
308
950
  return;
309
951
  }
310
952
  if ("type" in result && result.type === "hosted") {
311
- window.location.assign(result.payment_url);
953
+ navigateToCheckout(result.payment_url);
312
954
  return;
313
955
  }
314
956
  if ("url" in result && result.url) {
315
- window.location.assign(result.url);
957
+ navigateToCheckout(result.url);
316
958
  return;
317
959
  }
318
960
  if ("type" in result && result.type === "stripe") {
@@ -385,9 +1027,9 @@ function CheckoutButton({
385
1027
  setProcessing(false);
386
1028
  onSuccess?.(id);
387
1029
  },
388
- onComplete: (completed) => {
1030
+ onComplete: (completed2) => {
389
1031
  setProcessing(false);
390
- onComplete?.(completed);
1032
+ onComplete?.(completed2);
391
1033
  },
392
1034
  onScheduled
393
1035
  });
@@ -427,962 +1069,2474 @@ function CheckoutButton({
427
1069
  error && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", children: error.message })
428
1070
  ] });
429
1071
  }
430
- var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
431
- function fmtPrice(cents, cur) {
432
- const s = SYMBOLS[cur] ?? cur + " ";
433
- const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
434
- return `${s}${val}`;
435
- }
436
- function isEnterprise(p) {
437
- return /enterprise|custom/i.test(p.name + p.code);
438
- }
439
- function v(name, fb) {
440
- return `var(${name}, ${fb})`;
441
- }
442
- var C = {
443
- bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
444
- cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
445
- hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
446
- border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
447
- radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
448
- text: v("--nozle-foreground", "#111827"),
449
- muted: v("--nozle-muted-foreground", "#6b7280"),
450
- mutedBg: v("--nozle-muted", "#f3f4f6"),
451
- hlFg: v("--nozle-primary-foreground", "#ffffff")
452
- };
453
- function Check() {
454
- return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
1072
+ var CheckoutContext = React.createContext(null);
1073
+ function useCheckout() {
1074
+ const ctx = React.useContext(CheckoutContext);
1075
+ if (ctx === null) {
1076
+ throw new Error("useCheckout must be used inside a <Checkout> component");
1077
+ }
1078
+ return ctx;
455
1079
  }
456
- function Toggle({ isAnnual, onChange }) {
457
- const pill = {
458
- display: "inline-flex",
459
- borderRadius: "9999px",
460
- padding: "4px",
461
- background: C.mutedBg,
462
- gap: "2px"
1080
+ function buildStripeAppearance() {
1081
+ if (typeof document === "undefined") {
1082
+ return { theme: "stripe" };
1083
+ }
1084
+ const style = getComputedStyle(document.documentElement);
1085
+ const get = (prop, fallback) => style.getPropertyValue(prop).trim() || fallback;
1086
+ return {
1087
+ theme: "stripe",
1088
+ variables: {
1089
+ colorPrimary: get("--nozle-primary", "#16a34a"),
1090
+ colorBackground: get("--nozle-background", "#f8fafc"),
1091
+ colorText: get("--nozle-foreground", "#1e293b"),
1092
+ colorDanger: get("--nozle-destructive", "#dc2626"),
1093
+ borderRadius: get("--nozle-radius", "0.5rem"),
1094
+ fontFamily: "inherit"
1095
+ }
463
1096
  };
464
- const tab = (active) => ({
465
- padding: "8px 20px",
466
- borderRadius: "9999px",
467
- border: "none",
468
- background: active ? C.cardBg : "transparent",
469
- color: active ? C.text : C.muted,
470
- fontWeight: active ? 600 : 400,
471
- fontSize: "0.875rem",
472
- cursor: "pointer",
473
- boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
474
- transition: "all 0.15s ease"
475
- });
476
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pill, children: [
477
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
478
- /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
479
- "Annual",
480
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
481
- marginLeft: "6px",
482
- fontSize: "0.7rem",
483
- fontWeight: 600,
484
- color: isAnnual ? C.hl : C.muted
485
- }, children: "Save 20%" })
486
- ] })
487
- ] }) });
488
1097
  }
489
- function Card({
490
- plan,
491
- features,
492
- isCurrent,
493
- isHighlighted,
494
- isEnt,
495
- enterpriseEmail,
496
- onSelect
1098
+ function CheckoutInner({
1099
+ submitLabel,
1100
+ onSuccess,
1101
+ onError,
1102
+ onReady,
1103
+ className,
1104
+ style,
1105
+ returnUrl,
1106
+ children
497
1107
  }) {
498
- const [hover, setHover] = React.useState(false);
499
- const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
500
- const interval = plan.interval === "yearly" ? "/year" : "/month";
501
- const handleClick = () => {
502
- if (isCurrent) return;
503
- if (isEnt && enterpriseEmail) {
504
- window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
505
- return;
1108
+ const stripe = reactStripeJs.useStripe();
1109
+ const elements = reactStripeJs.useElements();
1110
+ const [isProcessing, setIsProcessing] = React.useState(false);
1111
+ const [error, setError] = React.useState(null);
1112
+ const [isReady, setIsReady] = React.useState(false);
1113
+ const inFlight = React.useRef(false);
1114
+ async function confirmPayment() {
1115
+ if (!stripe || !elements || inFlight.current) return;
1116
+ inFlight.current = true;
1117
+ setIsProcessing(true);
1118
+ setError(null);
1119
+ try {
1120
+ const result = await stripe.confirmPayment({
1121
+ elements,
1122
+ confirmParams: { return_url: returnUrl ?? window.location.href },
1123
+ redirect: "if_required"
1124
+ });
1125
+ if (result.error) {
1126
+ const err = new Error(result.error.message ?? "Payment failed");
1127
+ setError(err);
1128
+ onError?.(err);
1129
+ } else if (result.paymentIntent?.status === "succeeded") {
1130
+ onSuccess?.(result.paymentIntent.id);
1131
+ }
1132
+ } catch {
1133
+ const failure = new Error(
1134
+ "Payment could not be confirmed. Check its status before retrying."
1135
+ );
1136
+ setError(failure);
1137
+ onError?.(failure);
1138
+ } finally {
1139
+ inFlight.current = false;
1140
+ setIsProcessing(false);
506
1141
  }
507
- onSelect?.();
508
- };
509
- return /* @__PURE__ */ jsxRuntime.jsxs(
510
- "div",
1142
+ }
1143
+ return /* @__PURE__ */ jsxRuntime.jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style, children: [
1144
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes nozle-skeleton-pulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }` }),
1145
+ !isReady && /* @__PURE__ */ jsxRuntime.jsx(
1146
+ "div",
1147
+ {
1148
+ "data-testid": "checkout-skeleton",
1149
+ style: {
1150
+ height: "200px",
1151
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1152
+ background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
1153
+ backgroundSize: "200% 100%",
1154
+ animation: "nozle-skeleton-pulse 1.5s ease-in-out infinite"
1155
+ }
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsxRuntime.jsx(
1159
+ reactStripeJs.PaymentElement,
1160
+ {
1161
+ onReady: () => {
1162
+ setIsReady(true);
1163
+ onReady?.();
1164
+ }
1165
+ }
1166
+ ),
1167
+ error !== null && /* @__PURE__ */ jsxRuntime.jsx(
1168
+ "p",
1169
+ {
1170
+ style: {
1171
+ color: "var(--nozle-destructive, #dc2626)",
1172
+ margin: "0.5rem 0 0"
1173
+ },
1174
+ children: error.message
1175
+ }
1176
+ ),
1177
+ children ?? /* @__PURE__ */ jsxRuntime.jsx(
1178
+ "button",
1179
+ {
1180
+ type: "button",
1181
+ onClick: () => void confirmPayment(),
1182
+ disabled: isProcessing || !stripe,
1183
+ "aria-busy": isProcessing,
1184
+ style: {
1185
+ padding: "0.75rem 1.5rem",
1186
+ borderRadius: "var(--nozle-radius, 0.5rem)",
1187
+ border: "none",
1188
+ background: "var(--nozle-primary, var(--primary))",
1189
+ color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1190
+ cursor: isProcessing ? "not-allowed" : "pointer",
1191
+ fontWeight: 500,
1192
+ opacity: isProcessing ? 0.7 : 1,
1193
+ marginTop: "1rem",
1194
+ width: "100%"
1195
+ },
1196
+ children: isProcessing ? "Processing..." : submitLabel ?? "Pay now"
1197
+ }
1198
+ )
1199
+ ] }) });
1200
+ }
1201
+ function StripeCheckout({
1202
+ clientSecret,
1203
+ publishableKey,
1204
+ stripeAccount,
1205
+ returnUrl,
1206
+ submitLabel,
1207
+ onSuccess,
1208
+ onComplete,
1209
+ onError,
1210
+ onReady,
1211
+ className,
1212
+ style,
1213
+ children
1214
+ }) {
1215
+ const stripePromise = React.useMemo(
1216
+ () => stripeJs.loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1217
+ [publishableKey, stripeAccount]
1218
+ );
1219
+ const appearance = React.useMemo(() => buildStripeAppearance(), []);
1220
+ const embeddedOptions = React.useMemo(
1221
+ () => ({ clientSecret, onComplete }),
1222
+ [clientSecret, onComplete]
1223
+ );
1224
+ if (clientSecret.startsWith("cs_")) {
1225
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: /* @__PURE__ */ jsxRuntime.jsx(
1226
+ reactStripeJs.EmbeddedCheckoutProvider,
1227
+ {
1228
+ stripe: stripePromise,
1229
+ options: embeddedOptions,
1230
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckout, {})
1231
+ }
1232
+ ) });
1233
+ }
1234
+ const options = { clientSecret, appearance };
1235
+ return /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsxRuntime.jsx(
1236
+ CheckoutInner,
511
1237
  {
512
- onMouseEnter: () => setHover(true),
513
- onMouseLeave: () => setHover(false),
514
- style: {
515
- position: "relative",
516
- display: "flex",
517
- flexDirection: "column",
518
- background: C.cardBg,
519
- border: `1px solid ${isHighlighted ? C.hl : C.border}`,
520
- borderTopWidth: isHighlighted ? "3px" : "1px",
521
- borderRadius: C.radius,
522
- padding: "2rem",
523
- transition: "box-shadow 0.2s ease, transform 0.2s ease",
524
- boxShadow: shadow,
525
- transform: hover && !isCurrent ? "translateY(-2px)" : "none"
526
- },
527
- children: [
528
- isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
529
- position: "absolute",
530
- top: "-1px",
531
- left: "50%",
532
- transform: "translate(-50%, -50%)",
533
- background: C.hl,
534
- color: C.hlFg,
535
- fontSize: "0.7rem",
536
- fontWeight: 700,
537
- padding: "4px 12px",
538
- borderRadius: "9999px",
539
- letterSpacing: "0.025em",
540
- textTransform: "uppercase",
541
- whiteSpace: "nowrap"
542
- }, children: isCurrent ? "Current Plan" : "Most Popular" }),
543
- isCurrent && !isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
544
- position: "absolute",
545
- top: "0",
546
- left: "50%",
547
- transform: "translate(-50%, -50%)",
548
- background: C.mutedBg,
549
- color: C.muted,
550
- fontSize: "0.7rem",
551
- fontWeight: 700,
552
- padding: "4px 12px",
553
- borderRadius: "9999px",
554
- letterSpacing: "0.025em",
555
- textTransform: "uppercase",
556
- whiteSpace: "nowrap"
557
- }, children: "Current Plan" }),
558
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
559
- /* @__PURE__ */ jsxRuntime.jsx("h3", { style: {
560
- fontSize: "1.25rem",
561
- fontWeight: 700,
562
- color: C.text,
563
- margin: 0,
564
- lineHeight: 1.3
565
- }, children: plan.name }),
566
- plan.description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
567
- fontSize: "0.875rem",
568
- color: C.muted,
569
- margin: "6px 0 0",
570
- lineHeight: 1.5
571
- }, children: plan.description })
572
- ] }),
573
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
574
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
575
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
576
- ] }) }),
577
- features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { style: {
578
- listStyle: "none",
579
- padding: 0,
580
- margin: "0 0 2rem",
581
- display: "flex",
582
- flexDirection: "column",
583
- gap: "0.75rem",
584
- flex: 1
585
- }, children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { style: {
586
- display: "flex",
587
- alignItems: "flex-start",
588
- gap: "10px",
589
- fontSize: "0.875rem",
590
- color: C.text,
591
- lineHeight: 1.5
592
- }, children: [
593
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
594
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feat })
595
- ] }, feat)) }),
596
- (!features || features.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
597
- onSelect && /* @__PURE__ */ jsxRuntime.jsx(
598
- CTAButton,
599
- {
600
- onClick: handleClick,
601
- disabled: isCurrent,
602
- variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
603
- children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
604
- }
605
- )
606
- ]
1238
+ submitLabel,
1239
+ onSuccess,
1240
+ onError,
1241
+ onReady,
1242
+ className,
1243
+ style,
1244
+ returnUrl,
1245
+ children
607
1246
  }
608
- );
1247
+ ) }, clientSecret);
609
1248
  }
610
- function CTAButton({
611
- children,
612
- onClick,
613
- disabled,
614
- variant
615
- }) {
616
- const [hover, setHover] = React.useState(false);
617
- const base = {
618
- width: "100%",
619
- padding: "12px 24px",
620
- borderRadius: "10px",
621
- fontSize: "0.9375rem",
622
- fontWeight: 600,
623
- cursor: disabled ? "default" : "pointer",
624
- border: "none",
625
- transition: "all 0.15s ease",
626
- lineHeight: 1.4
627
- };
628
- const variants = {
629
- primary: {
630
- background: hover && !disabled ? "#4f46e5" : C.hl,
631
- color: C.hlFg
632
- },
633
- secondary: {
634
- background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
635
- color: C.text,
636
- border: `1px solid ${C.border}`
637
- },
638
- outline: {
639
- background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
640
- color: C.hl,
641
- border: `1.5px solid ${C.hl}`
642
- },
643
- current: {
644
- background: C.mutedBg,
645
- color: C.muted
1249
+ function ProviderCheckout(props) {
1250
+ const billing = useOptionalBillingContext();
1251
+ const [busy, setBusy] = React.useState(false);
1252
+ const [error, setError] = React.useState(null);
1253
+ const [processing, setProcessing] = React.useState(false);
1254
+ const pay = async () => {
1255
+ if (busy) return;
1256
+ setBusy(true);
1257
+ setError(null);
1258
+ try {
1259
+ await handleCheckoutResult(props.checkout, {
1260
+ verifyCheckout: props.verifyCheckout ?? billing?.verifyCheckout,
1261
+ getCheckoutStatus: props.getCheckoutStatus ?? billing?.getCheckoutStatus,
1262
+ onDismiss: props.onDismiss,
1263
+ onSuccess: (id) => {
1264
+ setProcessing(false);
1265
+ props.onSuccess?.(id);
1266
+ props.onComplete?.();
1267
+ },
1268
+ onComplete: () => {
1269
+ setProcessing(false);
1270
+ props.onComplete?.();
1271
+ },
1272
+ onScheduled: () => props.onComplete?.(),
1273
+ onProcessing: (status) => {
1274
+ setProcessing(true);
1275
+ props.onProcessing?.(status);
1276
+ }
1277
+ });
1278
+ } catch (cause) {
1279
+ const failure = cause instanceof Error ? cause : new Error("Checkout failed");
1280
+ setError(failure);
1281
+ props.onError?.(failure);
1282
+ } finally {
1283
+ setBusy(false);
646
1284
  }
647
1285
  };
1286
+ if ("type" in props.checkout && props.checkout.type === "stripe" && !props.checkout.url) {
1287
+ const result = props.checkout;
1288
+ const clientSecret = result.clientSecret ?? result.client_secret;
1289
+ if (clientSecret && result.publishable_key) {
1290
+ return /* @__PURE__ */ jsxRuntime.jsx(
1291
+ StripeCheckout,
1292
+ {
1293
+ clientSecret,
1294
+ publishableKey: result.publishable_key,
1295
+ stripeAccount: result.stripe_account,
1296
+ returnUrl: props.returnUrl,
1297
+ submitLabel: props.submitLabel,
1298
+ onSuccess: props.onSuccess,
1299
+ onComplete: props.onComplete,
1300
+ onError: props.onError
1301
+ }
1302
+ );
1303
+ }
1304
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: "Payment setup is incomplete. Contact support to continue." });
1305
+ }
648
1306
  return /* @__PURE__ */ jsxRuntime.jsx(
649
- "button",
1307
+ CheckoutContext.Provider,
650
1308
  {
651
- onClick: disabled ? void 0 : onClick,
652
- disabled,
653
- onMouseEnter: () => setHover(true),
654
- onMouseLeave: () => setHover(false),
655
- style: { ...base, ...variants[variant] },
656
- children
1309
+ value: { confirmPayment: pay, isProcessing: busy, error },
1310
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: props.className, children: [
1311
+ /* @__PURE__ */ jsxRuntime.jsx(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ disabled: busy,
1316
+ "aria-busy": busy,
1317
+ onClick: () => void pay(),
1318
+ children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
1319
+ }
1320
+ ),
1321
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1322
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error.message })
1323
+ ] })
657
1324
  }
658
1325
  );
659
1326
  }
660
- function Skeleton() {
661
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
662
- display: "grid",
663
- gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
664
- gap: "1.5rem"
665
- }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
666
- background: C.cardBg,
667
- border: `1px solid ${C.border}`,
668
- borderRadius: C.radius,
669
- padding: "2rem",
670
- height: "380px"
671
- }, children: [
672
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
673
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
674
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
675
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
676
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
677
- ] }, i)) });
1327
+ function Checkout(props) {
1328
+ return "checkout" in props ? /* @__PURE__ */ jsxRuntime.jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(StripeCheckout, { ...props });
678
1329
  }
679
- function PricingTable({
680
- currentPlanCode,
681
- plans: propPlans,
682
- features,
683
- onSelect,
684
- returnUrl,
685
- onStripeClientSecret,
686
- onCheckoutResult,
687
- onCheckoutError,
688
- showToggle = true,
689
- highlightPlan,
690
- className,
691
- enterpriseEmail
692
- }) {
1330
+ var styles2 = `
1331
+ .nozle-upgrade-modal{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;color:var(--nozle-portal-text,#202922);font:inherit}
1332
+ .nozle-upgrade-modal .nzu-dialog{box-sizing:border-box;background:var(--nozle-portal-background,#fff);border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:12px;padding:24px;max-width:480px;width:100%;max-height:90dvh;overflow:auto;box-shadow:0 12px 48px #0003}
1333
+ .nozle-upgrade-modal h2{font-size:20px;margin:0 0 16px}
1334
+ .nozle-upgrade-modal p{line-height:1.55}
1335
+ .nozle-upgrade-modal button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
1336
+ .nozle-upgrade-modal button:disabled{opacity:.55;cursor:wait}
1337
+ .nozle-upgrade-modal :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
1338
+ .nozle-upgrade-modal .nzu-buttons{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;margin-top:20px}
1339
+ .nozle-upgrade-modal [role=alert]{color:#922e25}
1340
+ `;
1341
+ function UpgradeModal(props) {
693
1342
  const billing = useOptionalBillingContext();
694
- const client = billing?.client;
695
- const [plans, setPlans] = React.useState(propPlans ?? []);
696
- const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
697
- const [isAnnual, setIsAnnual] = React.useState(false);
698
- const [loading, setLoading] = React.useState(!propPlans);
699
1343
  const [processing, setProcessing] = React.useState(false);
700
- const [checkoutError, setCheckoutError] = React.useState(null);
1344
+ const [error, setError] = React.useState(null);
1345
+ const [confirming, setConfirming] = React.useState(false);
1346
+ const inFlight = React.useRef(false);
1347
+ const dialog = React.useRef(null);
1348
+ const titleId = React.useId();
1349
+ const busy = Boolean(props.busy || confirming);
701
1350
  React.useEffect(() => {
702
- if (propPlans) {
703
- setPlans(propPlans);
704
- return;
705
- }
706
- if (!client) {
707
- setLoading(false);
708
- setCheckoutError("BillingProvider with a publishableKey is required to load plans");
709
- return;
1351
+ if (!props.isOpen) return;
1352
+ const previous = document.activeElement;
1353
+ setError(null);
1354
+ dialog.current?.focus();
1355
+ return () => {
1356
+ if (previous?.isConnected) previous.focus();
1357
+ };
1358
+ }, [props.isOpen]);
1359
+ async function handleConfirm() {
1360
+ if (inFlight.current || props.confirmDisabled || busy) return;
1361
+ inFlight.current = true;
1362
+ setConfirming(true);
1363
+ setError(null);
1364
+ try {
1365
+ if (props.onSubmit) {
1366
+ await props.onSubmit();
1367
+ } else {
1368
+ if (!billing?.createCheckout)
1369
+ throw new Error(
1370
+ "BillingProvider createCheckout callback is required for checkout"
1371
+ );
1372
+ const result = await billing.createCheckout({
1373
+ planCode: props.planCode,
1374
+ returnUrl: props.returnUrl ?? window.location.href,
1375
+ ...props.subscriptionId && { subscriptionId: props.subscriptionId },
1376
+ ...props.quoteId && { quoteId: props.quoteId },
1377
+ ...props.idempotencyKey && { idempotencyKey: props.idempotencyKey }
1378
+ });
1379
+ await handleCheckoutResult(result, {
1380
+ verifyCheckout: billing.verifyCheckout,
1381
+ getCheckoutStatus: billing.getCheckoutStatus,
1382
+ onSuccess: () => {
1383
+ setProcessing(false);
1384
+ props.onCompleted?.();
1385
+ },
1386
+ onProcessing: () => setProcessing(true),
1387
+ onStripeClientSecret: (secret) => {
1388
+ props.onStripeClientSecret?.(secret);
1389
+ props.onCheckoutStarted?.();
1390
+ },
1391
+ onComplete: () => {
1392
+ setProcessing(false);
1393
+ props.onCompleted?.();
1394
+ },
1395
+ onScheduled: () => props.onScheduled?.()
1396
+ });
1397
+ }
1398
+ props.onConfirm?.();
1399
+ } catch (cause) {
1400
+ const failure = cause instanceof Error ? cause : new Error("Checkout failed");
1401
+ setError(failure.message);
1402
+ props.onError?.(failure);
1403
+ } finally {
1404
+ inFlight.current = false;
1405
+ setConfirming(false);
710
1406
  }
711
- let cancelled = false;
712
- client.catalogFetch("/api/v1/plans").then(async (res) => {
713
- if (!cancelled && res.ok) {
714
- const data = await res.json();
715
- setPlans(data.plans || []);
1407
+ }
1408
+ if (!props.isOpen) return null;
1409
+ const money2 = (value) => new Intl.NumberFormat(props.locale ?? "en-US", {
1410
+ style: "currency",
1411
+ currency: props.preview?.currency ?? "USD"
1412
+ }).format(value);
1413
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nozle-upgrade-modal", children: [
1414
+ /* @__PURE__ */ jsxRuntime.jsx("style", { nonce: props.nonce, children: styles2 }),
1415
+ /* @__PURE__ */ jsxRuntime.jsxs(
1416
+ "div",
1417
+ {
1418
+ ref: dialog,
1419
+ className: "nzu-dialog",
1420
+ role: "dialog",
1421
+ "aria-modal": "true",
1422
+ "aria-labelledby": titleId,
1423
+ tabIndex: -1,
1424
+ "aria-busy": busy,
1425
+ onKeyDown: (event) => {
1426
+ if (event.key === "Escape" && !busy) {
1427
+ event.preventDefault();
1428
+ props.onCancel?.();
1429
+ }
1430
+ if (event.key !== "Tab") return;
1431
+ const nodes = Array.from(
1432
+ dialog.current?.querySelectorAll(
1433
+ 'button:not(:disabled),input:not(:disabled),select:not(:disabled),a[href],[tabindex="0"]'
1434
+ ) ?? []
1435
+ );
1436
+ const first = nodes[0], last = nodes[nodes.length - 1];
1437
+ if (!first) {
1438
+ event.preventDefault();
1439
+ return;
1440
+ }
1441
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
1442
+ event.preventDefault();
1443
+ last.focus();
1444
+ }
1445
+ if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog.current)) {
1446
+ event.preventDefault();
1447
+ first.focus();
1448
+ }
1449
+ },
1450
+ children: [
1451
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, children: props.title ?? "Confirm plan change" }),
1452
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1453
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error }),
1454
+ props.preview && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1455
+ props.preview.credit > 0 && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1456
+ "Credits applied: ",
1457
+ money2(-props.preview.credit)
1458
+ ] }),
1459
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1460
+ "New plan charge: ",
1461
+ money2(props.preview.debit)
1462
+ ] }),
1463
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
1464
+ "Due today: ",
1465
+ money2(props.preview.net)
1466
+ ] }) }),
1467
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1468
+ "Next billing date:",
1469
+ " ",
1470
+ new Date(props.preview.nextBillingDate).toLocaleDateString(
1471
+ props.locale
1472
+ ),
1473
+ "."
1474
+ ] })
1475
+ ] }),
1476
+ props.children,
1477
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzu-buttons", children: [
1478
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: props.onCancel, disabled: busy, children: "Close" }),
1479
+ /* @__PURE__ */ jsxRuntime.jsx(
1480
+ "button",
1481
+ {
1482
+ type: "button",
1483
+ onClick: () => void handleConfirm(),
1484
+ disabled: busy || props.confirmDisabled,
1485
+ children: busy ? "Checking subscription\u2026" : props.confirmLabel ?? "Confirm"
1486
+ }
1487
+ )
1488
+ ] })
1489
+ ]
716
1490
  }
717
- }).finally(() => {
718
- if (!cancelled) setLoading(false);
1491
+ )
1492
+ ] });
1493
+ }
1494
+
1495
+ // src/components/billing/amounts.ts
1496
+ function formatMinorAmount(value, currency, locale = "en-US") {
1497
+ const raw = String(value);
1498
+ if (!/^-?\d+$/.test(raw) || typeof value === "number" && !Number.isSafeInteger(value)) {
1499
+ return "Amount unavailable";
1500
+ }
1501
+ try {
1502
+ const formatter = new Intl.NumberFormat(locale, {
1503
+ style: "currency",
1504
+ currency
1505
+ });
1506
+ const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
1507
+ const minor = BigInt(raw);
1508
+ const absolute = minor < 0n ? -minor : minor;
1509
+ const scale = 10n ** BigInt(digits);
1510
+ const whole = absolute / scale;
1511
+ const signed = minor < 0n ? whole === 0n ? -0 : -whole : whole;
1512
+ const decimal = String(absolute % scale).padStart(digits, "0");
1513
+ const digitFormatter = new Intl.NumberFormat(locale, {
1514
+ useGrouping: false
1515
+ });
1516
+ const fraction = [...decimal].map((digit) => digitFormatter.format(Number(digit))).join("");
1517
+ return formatter.formatToParts(signed).map((part) => part.type === "fraction" ? fraction : part.value).join("");
1518
+ } catch {
1519
+ return "Amount unavailable";
1520
+ }
1521
+ }
1522
+ function formatBillingDate(value, locale = "en-US", timezone) {
1523
+ if (!value || !Number.isFinite(Date.parse(value))) return "Not available";
1524
+ try {
1525
+ return new Intl.DateTimeFormat(locale, {
1526
+ dateStyle: "medium",
1527
+ timeZone: timezone?.startsWith("TZ_") ? portalTimezones[timezone] ?? "UTC" : timezone ?? "UTC"
1528
+ }).format(new Date(value));
1529
+ } catch {
1530
+ return new Intl.DateTimeFormat(locale, {
1531
+ dateStyle: "medium",
1532
+ timeZone: "UTC"
1533
+ }).format(new Date(value));
1534
+ }
1535
+ }
1536
+ var styles3 = `
1537
+ .nozle-plan-change{font:inherit;color:var(--nozle-portal-text,#202922);margin-top:12px}
1538
+ .nozle-plan-change button{font:inherit;padding:9px 14px;border:1px solid var(--nozle-portal-border,#dce2d9);border-radius:7px;background:var(--nozle-portal-background,#fff);color:inherit;cursor:pointer}
1539
+ .nozle-plan-change button:disabled{opacity:.55;cursor:wait}
1540
+ .nozle-plan-change :focus-visible{outline:2px solid var(--nozle-portal-accent,#526949);outline-offset:3px}
1541
+ .nozle-plan-change [role=alert]{color:#922e25}
1542
+ .nozle-plan-change .nzpc-actions{display:flex;gap:10px;flex-wrap:wrap}
1543
+ .nozle-upgrade-modal select{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:10px;margin:8px 0;border:1px solid #dce2d9;border-radius:7px;background:var(--nozle-portal-background,#fff)}
1544
+ .nozle-upgrade-modal dl{display:block;margin:16px 0;padding:0}
1545
+ .nozle-upgrade-modal dl div{display:flex;justify-content:space-between;gap:16px;margin:12px 0}
1546
+ .nozle-upgrade-modal dd{margin:0;text-align:right}
1547
+ `;
1548
+ var paymentPending = (state) => ["awaiting_payment", "processing", "needs_review"].includes(
1549
+ state.checkoutStatus
1550
+ );
1551
+ function paymentMessage(state) {
1552
+ if (state.checkoutStatus === "needs_review")
1553
+ return "Payment needs review. Contact support before trying another payment.";
1554
+ return state.checkoutStatus === "awaiting_payment" ? "Complete payment to change your plan." : "Your change is processing. Your current plan remains active until confirmation.";
1555
+ }
1556
+ function done(attempt, state) {
1557
+ if (attempt.operation === "withdraw")
1558
+ return !state.pendingChange && state.status === "active" && state.currentPlan.code === attempt.fromPlan;
1559
+ return state.currentPlan.code === attempt.target && state.status === "active" || state.pendingChange?.plan.code === attempt.target;
1560
+ }
1561
+ function PlanChangeControl(props) {
1562
+ return /* @__PURE__ */ jsxRuntime.jsx(PlanChangeFlow, { ...props }, props.subscriptionId);
1563
+ }
1564
+ function PlanChangeFlow({
1565
+ subscriptionId,
1566
+ refreshKey,
1567
+ actions,
1568
+ onChanged,
1569
+ onError,
1570
+ returnUrl,
1571
+ locale = "en-US",
1572
+ timezone = "UTC",
1573
+ nonce
1574
+ }) {
1575
+ const owner = React.useMemo(
1576
+ () => ({ controller: new AbortController() }),
1577
+ [subscriptionId, actions]
1578
+ );
1579
+ const latest = React.useRef(owner);
1580
+ latest.current = owner;
1581
+ const [loaded, setLoaded] = React.useState();
1582
+ const [busy, setBusy] = React.useState(false);
1583
+ const [failure, setFailure] = React.useState();
1584
+ const [notice, setNotice] = React.useState("");
1585
+ const [mode, setMode] = React.useState(
1586
+ null
1587
+ );
1588
+ const [target, setTarget] = React.useState("");
1589
+ const [quote, setQuote] = React.useState();
1590
+ const [checkout, setCheckout] = React.useState(null);
1591
+ const [withdrawTarget, setWithdrawTarget] = React.useState();
1592
+ const pending = React.useRef(void 0);
1593
+ const previousRefresh = React.useRef(refreshKey);
1594
+ const inFlight = React.useRef(null);
1595
+ const state = loaded?.owner === owner ? loaded.value : void 0;
1596
+ const errorCallback = React.useRef(onError), changedCallback = React.useRef(onChanged);
1597
+ errorCallback.current = onError;
1598
+ changedCallback.current = onChanged;
1599
+ async function run(task) {
1600
+ if (latest.current !== owner || inFlight.current) return;
1601
+ const controller = new AbortController();
1602
+ inFlight.current = controller;
1603
+ setBusy(true);
1604
+ setFailure(void 0);
1605
+ let timedOut = false;
1606
+ let rejectAbort = () => {
1607
+ };
1608
+ const aborted = new Promise((_, reject) => {
1609
+ rejectAbort = () => reject(new Error("aborted"));
1610
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
1611
+ });
1612
+ const timer = setTimeout(() => {
1613
+ timedOut = true;
1614
+ controller.abort();
1615
+ }, 3e4);
1616
+ try {
1617
+ await Promise.race([task(controller.signal), aborted]);
1618
+ } catch (error) {
1619
+ if (latest.current !== owner || controller.signal.aborted && !timedOut)
1620
+ return;
1621
+ const safe = error instanceof BillingPortalError ? error : new BillingPortalError("request");
1622
+ if (safe.code === "changed") {
1623
+ pending.current = void 0;
1624
+ setQuote(void 0);
1625
+ setTarget("");
1626
+ setMode("choose");
1627
+ setFailure(
1628
+ "Your subscription or price changed. Check the latest details and confirm again."
1629
+ );
1630
+ } else
1631
+ setFailure(
1632
+ timedOut ? "This request timed out. Check subscription status before retrying." : safe.code === "unauthorized" ? "Your session expired. Reconnect to manage your subscription." : "We could not confirm this change. Check subscription status before retrying."
1633
+ );
1634
+ errorCallback.current?.(safe);
1635
+ } finally {
1636
+ clearTimeout(timer);
1637
+ controller.signal.removeEventListener("abort", rejectAbort);
1638
+ if (inFlight.current === controller) inFlight.current = null;
1639
+ if (latest.current === owner) setBusy(false);
1640
+ }
1641
+ }
1642
+ async function read(signal, initial = false) {
1643
+ const value = await (initial ? actions.load : actions.status)({
1644
+ subscriptionId,
1645
+ signal
1646
+ });
1647
+ if (signal.aborted || latest.current !== owner) return null;
1648
+ if (value.subscriptionId !== subscriptionId)
1649
+ throw new BillingPortalError("request");
1650
+ setLoaded({ owner, value });
1651
+ setCheckout(paymentPending(value) ? value.checkout : null);
1652
+ return value;
1653
+ }
1654
+ function finish(attempt, value) {
1655
+ pending.current = void 0;
1656
+ setMode(null);
1657
+ setQuote(void 0);
1658
+ setCheckout(null);
1659
+ setNotice(
1660
+ attempt.operation === "withdraw" ? "Scheduled change removed. Your current plan will continue to renew." : value.pendingChange ? `Plan change scheduled for ${formatBillingDate(value.pendingChange.effectiveAt, locale, timezone)}.` : `Your ${value.currentPlan.name} plan is active.`
1661
+ );
1662
+ changedCallback.current?.();
1663
+ }
1664
+ async function reconcile(signal) {
1665
+ const value = await read(signal);
1666
+ if (!value) return null;
1667
+ const attempt = pending.current;
1668
+ if (attempt && done(attempt, value)) finish(attempt, value);
1669
+ else if (!attempt && state && (value.currentPlan.code !== state.currentPlan.code || value.pendingChange?.id !== state.pendingChange?.id)) {
1670
+ setNotice(
1671
+ value.pendingChange ? `Plan change scheduled for ${formatBillingDate(value.pendingChange.effectiveAt, locale, timezone)}.` : `Your ${value.currentPlan.name} plan is active.`
1672
+ );
1673
+ changedCallback.current?.();
1674
+ } else if (["failed", "expired"].includes(value.checkoutStatus)) {
1675
+ pending.current = void 0;
1676
+ setCheckout(null);
1677
+ setMode(null);
1678
+ setNotice(
1679
+ "Payment did not complete. Your current plan is unchanged. You can choose a plan again."
1680
+ );
1681
+ } else if (paymentPending(value) || attempt?.accepted) {
1682
+ setNotice(paymentMessage(value));
1683
+ }
1684
+ return value;
1685
+ }
1686
+ React.useEffect(() => {
1687
+ inFlight.current?.abort();
1688
+ inFlight.current = null;
1689
+ if (owner.controller.signal.aborted)
1690
+ owner.controller = new AbortController();
1691
+ pending.current = void 0;
1692
+ setMode(null);
1693
+ setQuote(void 0);
1694
+ setCheckout(null);
1695
+ setNotice("");
1696
+ void run(async (signal) => {
1697
+ await read(signal, true);
719
1698
  });
720
1699
  return () => {
721
- cancelled = true;
1700
+ inFlight.current?.abort();
1701
+ owner.controller.abort();
722
1702
  };
723
- }, [client, propPlans]);
1703
+ }, [owner]);
724
1704
  React.useEffect(() => {
725
- if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
726
- }, [currentPlanCode]);
727
- async function selectPlan(plan) {
728
- setCheckoutError(null);
729
- if (onSelect) {
730
- onSelect(plan);
731
- return;
732
- }
733
- try {
734
- if (!billing?.createCheckout) {
735
- throw new Error("BillingProvider createCheckout callback is required for checkout");
1705
+ if (previousRefresh.current === refreshKey) return;
1706
+ previousRefresh.current = refreshKey;
1707
+ let stopped = false;
1708
+ let timer;
1709
+ const refresh = () => {
1710
+ if (stopped) return;
1711
+ if (inFlight.current) {
1712
+ timer = setTimeout(refresh, 100);
1713
+ return;
736
1714
  }
737
- const result = await billing.createCheckout({
738
- planCode: plan.code,
739
- returnUrl: returnUrl ?? window.location.href
740
- });
741
- onCheckoutResult?.(result);
742
- await handleCheckoutResult(result, {
743
- onStripeClientSecret,
744
- verifyCheckout: billing?.verifyCheckout,
745
- getCheckoutStatus: billing?.getCheckoutStatus,
746
- onProcessing: () => setProcessing(true),
747
- onSuccess: () => setProcessing(false),
748
- onComplete: () => setProcessing(false)
1715
+ void run(async (signal) => {
1716
+ await reconcile(signal);
749
1717
  });
750
- } catch (cause) {
751
- const error = cause instanceof Error ? cause : new Error("Checkout failed");
752
- setCheckoutError(error.message);
753
- onCheckoutError?.(error);
1718
+ };
1719
+ refresh();
1720
+ return () => {
1721
+ stopped = true;
1722
+ clearTimeout(timer);
1723
+ };
1724
+ }, [refreshKey, owner]);
1725
+ React.useEffect(() => {
1726
+ if (!state || state.checkoutStatus === "needs_review" || !paymentPending(state) && !pending.current?.accepted)
1727
+ return;
1728
+ const timer = setInterval(() => {
1729
+ if (!document.hidden)
1730
+ void run(async (signal) => {
1731
+ await reconcile(signal);
1732
+ });
1733
+ }, 5e3);
1734
+ return () => clearInterval(timer);
1735
+ }, [state, owner]);
1736
+ function openChange() {
1737
+ if (pending.current) {
1738
+ setMode(
1739
+ pending.current.operation === "withdraw" ? "withdraw" : "confirm"
1740
+ );
1741
+ return;
754
1742
  }
1743
+ setTarget("");
1744
+ setQuote(void 0);
1745
+ setMode("choose");
1746
+ setFailure(void 0);
1747
+ void run(async (signal) => {
1748
+ await read(signal);
1749
+ });
755
1750
  }
756
- const hasAnnual = plans.some((p) => p.interval === "yearly");
757
- const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
758
- if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, {});
759
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: {
760
- fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
761
- color: C.text,
762
- background: C.bg
763
- }, children: [
764
- showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
765
- processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
766
- checkoutError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: checkoutError }),
767
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
768
- display: "grid",
769
- gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
770
- gap: "1.5rem"
771
- }, children: filtered.map((plan, i) => /* @__PURE__ */ jsxRuntime.jsx(
772
- Card,
773
- {
774
- plan,
775
- features: features?.[i],
776
- isCurrent: currentPlan === plan.code,
777
- isHighlighted: highlightPlan === plan.code,
778
- isEnt: isEnterprise(plan),
779
- enterpriseEmail,
780
- onSelect: () => void selectPlan(plan)
781
- },
782
- plan.code
783
- )) })
1751
+ async function prepare() {
1752
+ await run(async (signal) => {
1753
+ const value = await read(signal);
1754
+ if (!value) return;
1755
+ if (value.endingAt || value.pendingChange || paymentPending(value) || !value.eligiblePlans.some((plan2) => plan2.code === target))
1756
+ throw new BillingPortalError("changed");
1757
+ const result = await actions.preview({
1758
+ subscriptionId,
1759
+ targetPlanCode: target,
1760
+ signal
1761
+ });
1762
+ if (signal.aborted) return;
1763
+ const plan = value.eligiblePlans.find((plan2) => plan2.code === target);
1764
+ if (result.operation !== plan.operation || !result.quoteToken || !Number.isFinite(Date.parse(result.effectiveAt)) || result.operation === "downgrade" && result.timing !== "end_of_period")
1765
+ throw new BillingPortalError("request");
1766
+ const amounts = [
1767
+ result.creditAmountCents,
1768
+ result.debitAmountCents,
1769
+ result.netAmountCents,
1770
+ result.amountDueNowCents,
1771
+ result.amountDueAtEffectiveCents
1772
+ ];
1773
+ if (!/^[A-Z]{3}$/.test(result.currency) || amounts.some(
1774
+ (amount) => formatMinorAmount(amount, result.currency, locale) === "Amount unavailable"
1775
+ ) || !["immediate", "end_of_period"].includes(result.timing) || result.renewalAt !== null && !Number.isFinite(Date.parse(result.renewalAt)))
1776
+ throw new BillingPortalError("request");
1777
+ setQuote(result);
1778
+ setMode("confirm");
1779
+ });
1780
+ }
1781
+ async function confirm() {
1782
+ if (mode === "choose") return prepare();
1783
+ await run(async (signal) => {
1784
+ let attempt = pending.current;
1785
+ if (!attempt) {
1786
+ if (mode === "withdraw") {
1787
+ if (!withdrawTarget) throw new BillingPortalError("changed");
1788
+ attempt = {
1789
+ key: crypto.randomUUID(),
1790
+ operation: "withdraw",
1791
+ pendingId: withdrawTarget.id,
1792
+ fromPlan: withdrawTarget.fromPlan,
1793
+ accepted: false,
1794
+ attempted: false
1795
+ };
1796
+ } else {
1797
+ if (!quote) throw new BillingPortalError("changed");
1798
+ attempt = {
1799
+ key: crypto.randomUUID(),
1800
+ operation: quote.operation,
1801
+ target,
1802
+ quote,
1803
+ accepted: false,
1804
+ attempted: false
1805
+ };
1806
+ }
1807
+ pending.current = attempt;
1808
+ }
1809
+ if (attempt.attempted) {
1810
+ await reconcile(signal);
1811
+ if (signal.aborted || pending.current !== attempt || attempt.accepted)
1812
+ return;
1813
+ }
1814
+ attempt.attempted = true;
1815
+ let result;
1816
+ try {
1817
+ if (attempt.operation === "withdraw")
1818
+ await actions.withdraw({
1819
+ subscriptionId,
1820
+ pendingChangeId: attempt.pendingId,
1821
+ idempotencyKey: attempt.key,
1822
+ signal
1823
+ });
1824
+ else
1825
+ result = await actions.apply({
1826
+ subscriptionId,
1827
+ targetPlanCode: attempt.target,
1828
+ quoteToken: attempt.quote.quoteToken,
1829
+ idempotencyKey: attempt.key,
1830
+ returnUrl: returnUrl ?? window.location.origin + window.location.pathname,
1831
+ signal
1832
+ });
1833
+ if (signal.aborted) return;
1834
+ attempt.accepted = true;
1835
+ setMode(null);
1836
+ } catch (error) {
1837
+ if (!signal.aborted) {
1838
+ await reconcile(signal).catch(() => null);
1839
+ if (pending.current !== attempt && !(error instanceof BillingPortalError && error.code === "changed"))
1840
+ return;
1841
+ }
1842
+ throw error;
1843
+ }
1844
+ const value = await reconcile(signal);
1845
+ if (!value || signal.aborted || pending.current !== attempt) return;
1846
+ if (result && !["completed", "scheduled"].includes(
1847
+ "type" in result ? result.type : ""
1848
+ ))
1849
+ setCheckout(result);
1850
+ });
1851
+ }
1852
+ const check = () => void run(async (signal) => {
1853
+ await reconcile(signal);
1854
+ });
1855
+ const selected = state?.eligiblePlans.find((plan) => plan.code === target);
1856
+ const money2 = (amount) => formatMinorAmount(
1857
+ amount,
1858
+ quote?.currency ?? state?.currentPlan.currency ?? "USD",
1859
+ locale
1860
+ );
1861
+ const blocked = Boolean(
1862
+ state?.endingAt || state?.pendingChange || state && paymentPending(state) || state?.blockedReason || pending.current?.accepted
1863
+ );
1864
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nozle-plan-change", children: [
1865
+ /* @__PURE__ */ jsxRuntime.jsx("style", { nonce, children: styles3 }),
1866
+ !state ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1867
+ /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Loading plan changes\u2026" }),
1868
+ failure && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: failure }),
1869
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: check, disabled: busy, children: "Check subscription status" })
1870
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1871
+ state.pendingChange && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1872
+ "Current plan until",
1873
+ " ",
1874
+ formatBillingDate(
1875
+ state.pendingChange.effectiveAt,
1876
+ locale,
1877
+ timezone
1878
+ ),
1879
+ ", then ",
1880
+ state.pendingChange.plan.name,
1881
+ " at",
1882
+ " ",
1883
+ formatMinorAmount(
1884
+ state.pendingChange.plan.amountCents,
1885
+ state.pendingChange.plan.currency,
1886
+ locale
1887
+ ),
1888
+ " ",
1889
+ "/ ",
1890
+ state.pendingChange.plan.interval,
1891
+ "."
1892
+ ] }),
1893
+ state.endingAt && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Keep your subscription before choosing another plan." }),
1894
+ state.blockedReason && !state.endingAt && !state.pendingChange && !paymentPending(state) && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Plan changes are currently unavailable. Contact support for your options." }),
1895
+ !state.blockedReason && !state.endingAt && !state.pendingChange && !paymentPending(state) && state.eligiblePlans.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No other plans are available for this subscription." }),
1896
+ ["failed", "expired"].includes(state.checkoutStatus) && !notice && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Payment did not complete. Your current plan is unchanged." }),
1897
+ paymentPending(state) && !notice && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: paymentMessage(state) }),
1898
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzpc-actions", children: [
1899
+ !blocked && state.status === "active" && state.eligiblePlans.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: openChange, children: "Change plan" }),
1900
+ state.pendingChange && state.status === "active" && !paymentPending(state) && /* @__PURE__ */ jsxRuntime.jsx(
1901
+ "button",
1902
+ {
1903
+ type: "button",
1904
+ disabled: busy,
1905
+ onClick: () => {
1906
+ setWithdrawTarget({
1907
+ id: state.pendingChange.id,
1908
+ name: state.pendingChange.plan.name,
1909
+ fromPlan: state.currentPlan.code,
1910
+ currentName: state.currentPlan.name
1911
+ });
1912
+ setMode("withdraw");
1913
+ setFailure(void 0);
1914
+ },
1915
+ children: "Keep current plan"
1916
+ }
1917
+ ),
1918
+ (paymentPending(state) || pending.current || failure) && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: check, children: "Check subscription status" }),
1919
+ pending.current?.attempted && !pending.current.accepted && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: openChange, children: "Review and retry change" })
1920
+ ] }),
1921
+ failure && !mode && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: failure }),
1922
+ notice && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: notice }),
1923
+ checkout && /* @__PURE__ */ jsxRuntime.jsx(
1924
+ Checkout,
1925
+ {
1926
+ checkout,
1927
+ returnUrl,
1928
+ verifyCheckout: actions.verifyCheckout ? (checkoutId, verification) => actions.verifyCheckout({
1929
+ subscriptionId,
1930
+ checkoutId,
1931
+ verification,
1932
+ signal: owner.controller.signal
1933
+ }) : void 0,
1934
+ getCheckoutStatus: actions.getCheckoutStatus ? (checkoutId) => actions.getCheckoutStatus({
1935
+ subscriptionId,
1936
+ checkoutId,
1937
+ signal: owner.controller.signal
1938
+ }) : void 0,
1939
+ onSuccess: check,
1940
+ onComplete: check,
1941
+ onProcessing: check,
1942
+ onDismiss: () => {
1943
+ if (owner.controller.signal.aborted) return;
1944
+ setNotice(
1945
+ "Payment was closed. Your plan changes only after payment is confirmed."
1946
+ );
1947
+ check();
1948
+ },
1949
+ onError: () => {
1950
+ if (owner.controller.signal.aborted) return;
1951
+ setFailure(
1952
+ "Payment could not be completed. Check subscription status to continue."
1953
+ );
1954
+ check();
1955
+ }
1956
+ },
1957
+ "checkout_id" in checkout ? checkout.checkout_id : "payment"
1958
+ ),
1959
+ /* @__PURE__ */ jsxRuntime.jsxs(
1960
+ UpgradeModal,
1961
+ {
1962
+ isOpen: mode !== null,
1963
+ planCode: target,
1964
+ title: mode === "withdraw" ? "Keep your current plan?" : mode === "choose" ? "Change plan" : `Confirm ${quote?.operation ?? "plan change"}`,
1965
+ nonce,
1966
+ busy,
1967
+ confirmDisabled: mode === "choose" && (!target || blocked) || mode === "confirm" && !quote,
1968
+ confirmLabel: pending.current?.attempted ? "Check status / retry" : mode === "choose" ? "Review change" : mode === "withdraw" ? "Confirm keep current plan" : quote?.operation === "downgrade" ? "Confirm downgrade" : "Confirm upgrade",
1969
+ onCancel: () => setMode(null),
1970
+ onSubmit: confirm,
1971
+ children: [
1972
+ failure && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: failure }),
1973
+ mode === "choose" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1974
+ "Choose a plan",
1975
+ /* @__PURE__ */ jsxRuntime.jsxs(
1976
+ "select",
1977
+ {
1978
+ value: target,
1979
+ onChange: (event) => {
1980
+ setTarget(event.target.value);
1981
+ setQuote(void 0);
1982
+ },
1983
+ disabled: busy,
1984
+ children: [
1985
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a plan" }),
1986
+ state.eligiblePlans.map((plan) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: plan.code, children: [
1987
+ plan.name,
1988
+ " \u2014",
1989
+ " ",
1990
+ formatMinorAmount(
1991
+ plan.amountCents,
1992
+ plan.currency,
1993
+ locale
1994
+ ),
1995
+ " ",
1996
+ "/ ",
1997
+ plan.interval,
1998
+ " (",
1999
+ plan.operation,
2000
+ ")"
2001
+ ] }, plan.code))
2002
+ ]
2003
+ }
2004
+ )
2005
+ ] }),
2006
+ mode === "withdraw" && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2007
+ "Remove the scheduled change to ",
2008
+ withdrawTarget?.name,
2009
+ ". Your",
2010
+ " ",
2011
+ withdrawTarget?.currentName,
2012
+ " plan will continue to renew."
2013
+ ] }),
2014
+ mode === "confirm" && quote && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2015
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2016
+ state.currentPlan.name,
2017
+ " \u2192 ",
2018
+ selected?.name ?? target
2019
+ ] }),
2020
+ selected && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2021
+ formatMinorAmount(
2022
+ selected.amountCents,
2023
+ selected.currency,
2024
+ locale
2025
+ ),
2026
+ " ",
2027
+ "/ ",
2028
+ selected.interval,
2029
+ ". Base price, excluding tax and usage."
2030
+ ] }),
2031
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { children: [
2032
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2033
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "New plan charge" }),
2034
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: money2(quote.debitAmountCents) })
2035
+ ] }),
2036
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2037
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Credits applied" }),
2038
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: money2(quote.creditAmountCents) })
2039
+ ] }),
2040
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2041
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Due now" }),
2042
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: money2(quote.amountDueNowCents) })
2043
+ ] }),
2044
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2045
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Due at effective date" }),
2046
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: money2(quote.amountDueAtEffectiveCents) })
2047
+ ] })
2048
+ ] }),
2049
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2050
+ "Effective",
2051
+ " ",
2052
+ formatBillingDate(quote.effectiveAt, locale, timezone),
2053
+ ".",
2054
+ " ",
2055
+ quote.renewalAt && `Next renewal ${formatBillingDate(quote.renewalAt, locale, timezone)}.`
2056
+ ] }),
2057
+ quote.timing === "end_of_period" ? /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your current plan and access continue until the effective date." }) : BigInt(quote.amountDueNowCents) === 0n ? /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No payment is due now for this change." }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your plan changes after the required payment is confirmed." })
2058
+ ] })
2059
+ ]
2060
+ }
2061
+ )
2062
+ ] })
784
2063
  ] });
785
2064
  }
786
- function PlanCard({
787
- id,
788
- name,
789
- monthlyPrice,
790
- annualPrice,
791
- features,
792
- description,
793
- isAnnual,
794
- isCurrent,
795
- returnUrl,
796
- onSelect
2065
+ function asError(error) {
2066
+ return error instanceof BillingPortalError ? error : new BillingPortalError("request");
2067
+ }
2068
+ function BillingPortal({
2069
+ createSession,
2070
+ cancellationActions,
2071
+ planChangeActions,
2072
+ locale = "en-US",
2073
+ className = "",
2074
+ style,
2075
+ nonce,
2076
+ onError
797
2077
  }) {
798
- const price = isAnnual ? annualPrice : monthlyPrice;
799
- const interval = isAnnual ? "year" : "month";
2078
+ const [attempt, setAttempt] = React.useState(0);
2079
+ const [state, setState] = React.useState();
2080
+ const errorCallback = React.useRef(onError);
2081
+ errorCallback.current = onError;
2082
+ React.useEffect(() => {
2083
+ const controller = new AbortController();
2084
+ let active = true;
2085
+ const timer = setTimeout(() => {
2086
+ active = false;
2087
+ controller.abort();
2088
+ const error = new BillingPortalError("session");
2089
+ setState({ source: createSession, attempt, error });
2090
+ errorCallback.current?.(error);
2091
+ }, 2e4);
2092
+ Promise.resolve().then(() => createSession({ signal: controller.signal })).then((session) => {
2093
+ if (active)
2094
+ setState({
2095
+ source: createSession,
2096
+ attempt,
2097
+ client: portalClient(session)
2098
+ });
2099
+ }).catch(() => {
2100
+ if (active) {
2101
+ const error = new BillingPortalError("session");
2102
+ setState({ source: createSession, attempt, error });
2103
+ errorCallback.current?.(error);
2104
+ }
2105
+ }).finally(() => clearTimeout(timer));
2106
+ return () => {
2107
+ active = false;
2108
+ clearTimeout(timer);
2109
+ controller.abort();
2110
+ };
2111
+ }, [createSession, attempt]);
2112
+ const current = state?.source === createSession && state.attempt === attempt ? state : void 0;
2113
+ const reconnect = () => setAttempt((value) => value + 1);
800
2114
  return /* @__PURE__ */ jsxRuntime.jsxs(
801
2115
  "div",
802
2116
  {
803
- "data-testid": `plan-card-${id}`,
804
- "aria-current": isCurrent ? "true" : void 0,
805
- style: {
806
- background: "var(--nozle-background, var(--background))",
807
- border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
808
- borderRadius: "var(--nozle-radius, 0.5rem)",
809
- padding: "1.5rem",
810
- display: "flex",
811
- flexDirection: "column",
812
- gap: "1rem"
813
- },
814
- className: isCurrent ? "border-primary" : "",
2117
+ className: `nozle-billing-portal ${className}`,
2118
+ style,
2119
+ "aria-label": "Nozle billing portal",
815
2120
  children: [
816
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
817
- /* @__PURE__ */ jsxRuntime.jsx(
818
- "h3",
819
- {
820
- style: {
821
- fontSize: "1.25rem",
822
- fontWeight: 600,
823
- color: "var(--nozle-foreground, var(--foreground))",
824
- margin: 0
825
- },
826
- children: name
827
- }
828
- ),
829
- description && /* @__PURE__ */ jsxRuntime.jsx(
830
- "p",
831
- {
832
- style: {
833
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
834
- fontSize: "0.875rem",
835
- margin: "0.25rem 0 0"
836
- },
837
- children: description
838
- }
839
- )
840
- ] }),
841
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
842
- /* @__PURE__ */ jsxRuntime.jsxs(
843
- "span",
844
- {
845
- style: {
846
- fontSize: "2rem",
847
- fontWeight: 700,
848
- color: "var(--nozle-foreground, var(--foreground))"
849
- },
850
- children: [
851
- "$",
852
- price
853
- ]
854
- }
855
- ),
856
- /* @__PURE__ */ jsxRuntime.jsxs(
857
- "span",
858
- {
859
- style: {
860
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
861
- marginLeft: "0.25rem"
862
- },
863
- children: [
864
- "/",
865
- interval
866
- ]
867
- }
868
- )
869
- ] }),
870
- /* @__PURE__ */ jsxRuntime.jsx(
871
- "ul",
872
- {
873
- style: {
874
- listStyle: "none",
875
- padding: 0,
876
- margin: 0,
877
- display: "flex",
878
- flexDirection: "column",
879
- gap: "0.5rem",
880
- flex: 1
881
- },
882
- children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
883
- "li",
884
- {
885
- style: {
886
- display: "flex",
887
- alignItems: "center",
888
- gap: "0.5rem",
889
- color: "var(--nozle-foreground, var(--foreground))",
890
- fontSize: "0.875rem"
891
- },
892
- children: [
893
- /* @__PURE__ */ jsxRuntime.jsx(
894
- "span",
895
- {
896
- style: { color: "var(--nozle-primary, var(--primary))" },
897
- "aria-hidden": "true",
898
- children: "\u2713"
899
- }
900
- ),
901
- feature
902
- ]
903
- },
904
- feature
905
- ))
906
- }
907
- ),
908
- onSelect && /* @__PURE__ */ jsxRuntime.jsx(
909
- "button",
2121
+ /* @__PURE__ */ jsxRuntime.jsx("style", { nonce, children: portalStyles }),
2122
+ current?.client ? /* @__PURE__ */ jsxRuntime.jsx(
2123
+ Portal,
910
2124
  {
911
- onClick: () => onSelect?.({
912
- planCode: id,
913
- returnUrl: returnUrl ?? window.location.href
914
- }),
915
- style: {
916
- background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
917
- color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
918
- border: "none",
919
- borderRadius: "var(--nozle-radius, 0.5rem)",
920
- padding: "0.75rem 1.5rem",
921
- cursor: isCurrent ? "default" : "pointer",
922
- fontWeight: 500,
923
- width: "100%"
924
- },
925
- disabled: isCurrent,
926
- "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
927
- children: isCurrent ? "Current Plan" : "Get Started"
928
- }
929
- )
2125
+ client: current.client,
2126
+ locale,
2127
+ reconnect,
2128
+ onError,
2129
+ cancellationActions,
2130
+ planChangeActions,
2131
+ nonce
2132
+ },
2133
+ attempt
2134
+ ) : current?.error ? /* @__PURE__ */ jsxRuntime.jsx(Failure, { error: current.error, retry: reconnect }) : /* @__PURE__ */ jsxRuntime.jsx(Loading, { label: "Starting billing session\u2026" })
930
2135
  ]
931
2136
  }
932
2137
  );
933
2138
  }
934
- function PlanComparison({
935
- plans,
936
- features
2139
+ function useQuery(client, query, variables, onError, retainDuringRefresh = false) {
2140
+ const [attempt, setAttempt] = React.useState(0);
2141
+ const key = JSON.stringify(variables);
2142
+ const [state, setState] = React.useState();
2143
+ const callback = React.useRef(onError);
2144
+ callback.current = onError;
2145
+ React.useEffect(() => {
2146
+ const controller = new AbortController();
2147
+ client.request(query, JSON.parse(key), controller.signal).then((data) => {
2148
+ if (!controller.signal.aborted)
2149
+ setState({ client, key, attempt, data });
2150
+ }).catch((cause) => {
2151
+ if (!controller.signal.aborted) {
2152
+ const error = asError(cause);
2153
+ setState({ client, key, attempt, error });
2154
+ callback.current?.(error);
2155
+ }
2156
+ });
2157
+ return () => controller.abort();
2158
+ }, [client, query, key, attempt]);
2159
+ const current = state?.client === client && state.key === key && state.attempt === attempt ? state : void 0;
2160
+ return {
2161
+ data: current?.data ?? (retainDuringRefresh && !current && state?.client === client && state.key === key ? state.data : void 0),
2162
+ error: current?.error,
2163
+ retry: () => setAttempt((n) => n + 1)
2164
+ };
2165
+ }
2166
+ function useAction(onError) {
2167
+ const [pending, setPending] = React.useState(false);
2168
+ const [error, setError] = React.useState();
2169
+ const controller = React.useRef(null);
2170
+ React.useEffect(() => () => controller.current?.abort(), []);
2171
+ return {
2172
+ pending,
2173
+ error,
2174
+ run: async (action) => {
2175
+ if (controller.current) return;
2176
+ const next = new AbortController();
2177
+ controller.current = next;
2178
+ setPending(true);
2179
+ setError(void 0);
2180
+ try {
2181
+ await action(next.signal);
2182
+ } catch (cause) {
2183
+ if (!next.signal.aborted) {
2184
+ const failure = asError(cause);
2185
+ setError(failure);
2186
+ onError?.(failure);
2187
+ }
2188
+ } finally {
2189
+ if (!next.signal.aborted) setPending(false);
2190
+ if (controller.current === next) controller.current = null;
2191
+ }
2192
+ }
2193
+ };
2194
+ }
2195
+ function Loading({ label = "Loading billing data\u2026" }) {
2196
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-empty", role: "status", children: label });
2197
+ }
2198
+ function Failure({
2199
+ error,
2200
+ retry,
2201
+ reconnect
937
2202
  }) {
938
- return /* @__PURE__ */ jsxRuntime.jsx(
939
- "div",
940
- {
941
- style: {
942
- overflowX: "auto",
943
- background: "var(--nozle-background, var(--background))"
944
- },
945
- children: /* @__PURE__ */ jsxRuntime.jsxs(
946
- "table",
2203
+ const action = error.code === "unauthorized" ? reconnect ?? retry : retry;
2204
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: "nzp-error", children: [
2205
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error.message }),
2206
+ action && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: action, children: error.code === "unauthorized" ? "Reconnect" : "Try again" })
2207
+ ] });
2208
+ }
2209
+ function money(cents, currency, locale) {
2210
+ const formatter = new Intl.NumberFormat(locale, {
2211
+ style: "currency",
2212
+ currency
2213
+ });
2214
+ const digits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
2215
+ return formatter.format(Number(cents) / 10 ** digits);
2216
+ }
2217
+ function date(value, locale, timeZone) {
2218
+ if (!value) return "\u2014";
2219
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
2220
+ const zone = timeZone?.startsWith("TZ_") ? portalTimezones[timeZone] ?? "UTC" : timeZone ?? "UTC";
2221
+ return new Intl.DateTimeFormat(locale, {
2222
+ dateStyle: "medium",
2223
+ timeZone: dateOnly ? "UTC" : zone
2224
+ }).format(new Date(value));
2225
+ }
2226
+ var planName = (subscription) => subscription.name || subscription.plan.invoiceDisplayName || subscription.plan.name;
2227
+ var intervals = {
2228
+ weekly: "week",
2229
+ monthly: "month",
2230
+ quarterly: "quarter",
2231
+ semiannual: "six months",
2232
+ yearly: "year"
2233
+ };
2234
+ function Portal(props) {
2235
+ const { client, locale, reconnect, onError } = props;
2236
+ const { data, error, retry } = useQuery(
2237
+ client,
2238
+ identityQuery,
2239
+ {},
2240
+ onError
2241
+ );
2242
+ const [view, setView] = React.useState({ kind: "overview" });
2243
+ const [notice, setNotice] = React.useState("");
2244
+ const back = () => setView({ kind: "overview" });
2245
+ if (error)
2246
+ return /* @__PURE__ */ jsxRuntime.jsx(Failure, { error, retry, reconnect });
2247
+ if (!data) return /* @__PURE__ */ jsxRuntime.jsx(Loading, {});
2248
+ const customer = data.customerPortalUser;
2249
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2250
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
2251
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-row", children: [
2252
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nzp-muted", children: data.customerPortalOrganization.name }),
2253
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: reconnect, children: "Refresh portal" })
2254
+ ] }),
2255
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Billing portal" }),
2256
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-muted", children: customer.name || [customer.firstname, customer.lastname].filter(Boolean).join(" ") || customer.email || "Your account" })
2257
+ ] }),
2258
+ notice && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: notice }),
2259
+ view.kind !== "overview" && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: back, children: "\u2190 Back to overview" }),
2260
+ view.kind === "overview" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2261
+ /* @__PURE__ */ jsxRuntime.jsx(
2262
+ Subscriptions,
2263
+ {
2264
+ ...props,
2265
+ timezone: customer.applicableTimezone,
2266
+ onUsage: (subscription) => setView({ kind: "usage", subscription })
2267
+ }
2268
+ ),
2269
+ /* @__PURE__ */ jsxRuntime.jsx(
2270
+ Wallets,
2271
+ {
2272
+ ...props,
2273
+ onWallet: (wallet) => setView({ kind: "wallet", wallet })
2274
+ }
2275
+ ),
2276
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Billing information", children: [
2277
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-row", children: [
2278
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Billing information" }),
2279
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setView({ kind: "edit" }), children: "Edit billing information" })
2280
+ ] }),
2281
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { children: [
2282
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2283
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Account name" }),
2284
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: customer.name || "Not provided" })
2285
+ ] }),
2286
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2287
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Email" }),
2288
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: customer.email || "Not provided" })
2289
+ ] }),
2290
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2291
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Legal name" }),
2292
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: customer.legalName || "Not provided" })
2293
+ ] }),
2294
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2295
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Tax ID" }),
2296
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: customer.taxIdentificationNumber || "Not provided" })
2297
+ ] }),
2298
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2299
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Billing address" }),
2300
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: [
2301
+ customer.addressLine1,
2302
+ customer.addressLine2,
2303
+ customer.city,
2304
+ customer.state,
2305
+ customer.zipcode,
2306
+ customer.country
2307
+ ].filter(Boolean).join(", ") || "Not provided" })
2308
+ ] })
2309
+ ] })
2310
+ ] }),
2311
+ /* @__PURE__ */ jsxRuntime.jsx(Invoices, { ...props })
2312
+ ] }),
2313
+ view.kind === "usage" && /* @__PURE__ */ jsxRuntime.jsx(
2314
+ UsageDetails,
2315
+ {
2316
+ ...props,
2317
+ subscription: view.subscription,
2318
+ timezone: customer.applicableTimezone
2319
+ }
2320
+ ),
2321
+ view.kind === "edit" && /* @__PURE__ */ jsxRuntime.jsx(
2322
+ CustomerForm,
2323
+ {
2324
+ ...props,
2325
+ customer,
2326
+ onSaved: () => {
2327
+ setNotice("Billing information saved.");
2328
+ back();
2329
+ retry();
2330
+ },
2331
+ onCancel: back
2332
+ }
2333
+ ),
2334
+ view.kind === "wallet" && /* @__PURE__ */ jsxRuntime.jsx(
2335
+ WalletTopUp,
2336
+ {
2337
+ ...props,
2338
+ wallet: view.wallet,
2339
+ onCreated: () => {
2340
+ setNotice(
2341
+ "Top-up requested. Your balance updates after payment is processed."
2342
+ );
2343
+ back();
2344
+ }
2345
+ }
2346
+ ),
2347
+ /* @__PURE__ */ jsxRuntime.jsx("footer", { children: "Powered by Nozle" })
2348
+ ] });
2349
+ }
2350
+ function Pager({
2351
+ page,
2352
+ total,
2353
+ setPage
2354
+ }) {
2355
+ if (total <= 1) return null;
2356
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-actions", "aria-label": "Pagination", children: [
2357
+ /* @__PURE__ */ jsxRuntime.jsx(
2358
+ "button",
2359
+ {
2360
+ type: "button",
2361
+ disabled: page <= 1,
2362
+ onClick: () => setPage(page - 1),
2363
+ children: "Previous"
2364
+ }
2365
+ ),
2366
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2367
+ "Page ",
2368
+ page,
2369
+ " of ",
2370
+ total
2371
+ ] }),
2372
+ /* @__PURE__ */ jsxRuntime.jsx(
2373
+ "button",
2374
+ {
2375
+ type: "button",
2376
+ disabled: page >= total,
2377
+ onClick: () => setPage(page + 1),
2378
+ children: "Next"
2379
+ }
2380
+ )
2381
+ ] });
2382
+ }
2383
+ function Subscriptions({
2384
+ client,
2385
+ locale,
2386
+ timezone,
2387
+ reconnect,
2388
+ onError,
2389
+ onUsage,
2390
+ cancellationActions,
2391
+ planChangeActions,
2392
+ nonce
2393
+ }) {
2394
+ const [page, setPage] = React.useState(1);
2395
+ const [updated, setUpdated] = React.useState({});
2396
+ const { data, error, retry } = useQuery(client, subscriptionsQuery, { page }, onError, true);
2397
+ const result = data?.customerPortalSubscriptions;
2398
+ React.useEffect(() => setUpdated({}), [result]);
2399
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Subscriptions", children: [
2400
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Subscriptions" }),
2401
+ error ? /* @__PURE__ */ jsxRuntime.jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsxRuntime.jsx(Loading, {}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2402
+ result.collection.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-empty", children: "No active subscriptions." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-stack", children: result.collection.map((item) => updated[item.id] ?? item).map((subscription) => /* @__PURE__ */ jsxRuntime.jsxs(
2403
+ "article",
947
2404
  {
948
- style: {
949
- width: "100%",
950
- borderCollapse: "collapse",
951
- fontSize: "0.875rem",
952
- color: "var(--nozle-foreground, var(--foreground))"
953
- },
2405
+ className: "nzp-card",
2406
+ "data-subscription-id": subscription.externalId,
954
2407
  children: [
955
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
956
- /* @__PURE__ */ jsxRuntime.jsx(
957
- "th",
958
- {
959
- style: {
960
- padding: "0.75rem 1rem",
961
- textAlign: "left",
962
- borderBottom: "1px solid var(--nozle-border, var(--border))",
963
- color: "var(--nozle-muted-foreground, var(--muted-foreground))",
964
- fontWeight: 500
965
- },
966
- children: "Feature"
967
- }
2408
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-row", children: [
2409
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: planName(subscription) }),
2410
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nzp-badge", children: subscription.endingAt ? "Scheduled to end" : "Active" })
2411
+ ] }),
2412
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-price", children: [
2413
+ money(
2414
+ subscription.plan.amountCents,
2415
+ subscription.plan.amountCurrency,
2416
+ locale
968
2417
  ),
969
- plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
970
- "th",
971
- {
972
- style: {
973
- padding: "0.75rem 1rem",
974
- textAlign: "center",
975
- borderBottom: "1px solid var(--nozle-border, var(--border))",
976
- fontWeight: 600,
977
- color: "var(--nozle-foreground, var(--foreground))"
978
- },
979
- children: plan.name
980
- },
981
- plan.id
982
- ))
983
- ] }) }),
984
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2418
+ " ",
2419
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "nzp-muted", children: [
2420
+ "/",
2421
+ " ",
2422
+ intervals[subscription.plan.interval] || subscription.plan.interval
2423
+ ] })
2424
+ ] }),
2425
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-row", children: [
2426
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "nzp-muted", children: [
2427
+ "Base price, excluding tax and usage",
2428
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
2429
+ "Current period ends",
2430
+ " ",
2431
+ date(
2432
+ subscription.currentBillingPeriodEndingAt,
2433
+ locale,
2434
+ timezone
2435
+ )
2436
+ ] }),
985
2437
  /* @__PURE__ */ jsxRuntime.jsx(
986
- "td",
2438
+ "button",
987
2439
  {
988
- style: {
989
- padding: "0.75rem 1rem",
990
- borderBottom: "1px solid var(--nozle-border, var(--border))",
991
- color: "var(--nozle-foreground, var(--foreground))"
992
- },
993
- children: feature.label
2440
+ type: "button",
2441
+ onClick: () => onUsage(subscription),
2442
+ children: "View usage"
994
2443
  }
995
- ),
996
- plans.map((plan) => {
997
- const value = feature.values[plan.id];
998
- return /* @__PURE__ */ jsxRuntime.jsx(
999
- "td",
1000
- {
1001
- style: {
1002
- padding: "0.75rem 1rem",
1003
- textAlign: "center",
1004
- borderBottom: "1px solid var(--nozle-border, var(--border))"
1005
- },
1006
- children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsxRuntime.jsx(
1007
- "span",
1008
- {
1009
- style: {
1010
- color: "var(--nozle-primary, var(--primary))"
1011
- },
1012
- "aria-label": "Included",
1013
- children: "\u2713"
1014
- }
1015
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1016
- "span",
1017
- {
1018
- style: {
1019
- color: "var(--nozle-muted-foreground, var(--muted-foreground))"
1020
- },
1021
- "aria-label": "Not included",
1022
- children: "\u2715"
1023
- }
1024
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: value ?? "\u2014" })
1025
- },
1026
- plan.id
1027
- );
1028
- })
1029
- ] }, feature.key)) })
2444
+ )
2445
+ ] }),
2446
+ subscription.endingAt && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2447
+ "Cancels on",
2448
+ " ",
2449
+ date(subscription.endingAt, locale, timezone)
2450
+ ] }),
2451
+ subscription.nextSubscription?.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2452
+ "Scheduled plan:",
2453
+ " ",
2454
+ subscription.nextSubscription.plan.name
2455
+ ] }),
2456
+ cancellationActions && subscription.externalId && /* @__PURE__ */ jsxRuntime.jsx(
2457
+ CancellationControl,
2458
+ {
2459
+ subscription: cancellationState(subscription),
2460
+ actions: cancellationActions,
2461
+ locale,
2462
+ timezone,
2463
+ nonce,
2464
+ onError,
2465
+ onChanged: retry,
2466
+ refresh: async (signal) => {
2467
+ const result2 = await client.request(
2468
+ subscriptionQuery,
2469
+ { id: subscription.id },
2470
+ signal
2471
+ );
2472
+ if (!signal.aborted && result2.customerPortalSubscription) {
2473
+ const fresh = result2.customerPortalSubscription;
2474
+ setUpdated((previous) => ({
2475
+ ...previous,
2476
+ [fresh.id]: fresh
2477
+ }));
2478
+ }
2479
+ return result2.customerPortalSubscription ? cancellationState(
2480
+ result2.customerPortalSubscription
2481
+ ) : null;
2482
+ }
2483
+ }
2484
+ ),
2485
+ planChangeActions && subscription.externalId && /* @__PURE__ */ jsxRuntime.jsx(
2486
+ PlanChangeControl,
2487
+ {
2488
+ subscriptionId: subscription.externalId,
2489
+ refreshKey: `${subscription.id}:${subscription.endingAt ?? ""}:${subscription.nextSubscription?.id ?? ""}:${subscription.plan.code}`,
2490
+ actions: planChangeActions,
2491
+ locale,
2492
+ timezone,
2493
+ nonce,
2494
+ onError,
2495
+ onChanged: retry
2496
+ }
2497
+ )
1030
2498
  ]
2499
+ },
2500
+ subscription.externalId || subscription.id
2501
+ )) }),
2502
+ /* @__PURE__ */ jsxRuntime.jsx(
2503
+ Pager,
2504
+ {
2505
+ page,
2506
+ total: result.metadata.totalPages,
2507
+ setPage
1031
2508
  }
1032
2509
  )
1033
- }
1034
- );
1035
- }
1036
- var CheckoutContext = React.createContext(null);
1037
- function useCheckout2() {
1038
- const ctx = React.useContext(CheckoutContext);
1039
- if (ctx === null) {
1040
- throw new Error("useCheckout must be used inside a <Checkout> component");
1041
- }
1042
- return ctx;
2510
+ ] })
2511
+ ] });
1043
2512
  }
1044
- function buildStripeAppearance() {
1045
- if (typeof document === "undefined") {
1046
- return { theme: "stripe" };
1047
- }
1048
- const style = getComputedStyle(document.documentElement);
1049
- const get = (prop, fallback) => style.getPropertyValue(prop).trim() || fallback;
2513
+ function cancellationState(subscription) {
1050
2514
  return {
1051
- theme: "stripe",
1052
- variables: {
1053
- colorPrimary: get("--nozle-primary", "#16a34a"),
1054
- colorBackground: get("--nozle-background", "#f8fafc"),
1055
- colorText: get("--nozle-foreground", "#1e293b"),
1056
- colorDanger: get("--nozle-destructive", "#dc2626"),
1057
- borderRadius: get("--nozle-radius", "0.5rem"),
1058
- fontFamily: "inherit"
1059
- }
2515
+ id: subscription.id,
2516
+ externalId: subscription.externalId,
2517
+ name: planName(subscription),
2518
+ status: subscription.status,
2519
+ endingAt: subscription.endingAt,
2520
+ pendingPlanName: subscription.nextSubscription?.status === "pending" ? subscription.nextSubscription.plan.name : null
1060
2521
  };
1061
2522
  }
1062
- function CheckoutInner({
1063
- submitLabel,
1064
- onSuccess,
2523
+ function UsageDetails({
2524
+ client,
2525
+ locale,
2526
+ reconnect,
1065
2527
  onError,
1066
- onReady,
1067
- className,
1068
- style,
1069
- returnUrl,
1070
- children
2528
+ subscription,
2529
+ timezone
1071
2530
  }) {
1072
- const stripe = reactStripeJs.useStripe();
1073
- const elements = reactStripeJs.useElements();
1074
- const [isProcessing, setIsProcessing] = React.useState(false);
1075
- const [error, setError] = React.useState(null);
1076
- const [isReady, setIsReady] = React.useState(false);
1077
- async function confirmPayment() {
1078
- if (!stripe || !elements) return;
1079
- setIsProcessing(true);
1080
- setError(null);
1081
- const result = await stripe.confirmPayment({
1082
- elements,
1083
- confirmParams: { return_url: returnUrl ?? window.location.href },
1084
- redirect: "if_required"
2531
+ const { data, error, retry } = useQuery(client, usageQuery, { subscriptionId: subscription.id }, onError);
2532
+ const usage = data?.customerPortalCustomerUsage;
2533
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Subscription usage", children: [
2534
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { children: [
2535
+ planName(subscription),
2536
+ " \xB7 Usage"
2537
+ ] }),
2538
+ error ? /* @__PURE__ */ jsxRuntime.jsx(Failure, { error, retry, reconnect }) : !usage ? /* @__PURE__ */ jsxRuntime.jsx(Loading, {}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2539
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "nzp-muted", children: [
2540
+ date(usage.fromDatetime, locale, timezone),
2541
+ " \u2013",
2542
+ " ",
2543
+ date(usage.toDatetime, locale, timezone)
2544
+ ] }),
2545
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-price", children: money(usage.amountCents, usage.currency, locale) }),
2546
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-muted", children: "Current usage, excluding tax" }),
2547
+ usage.chargesUsage.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-table", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
2548
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2549
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Feature" }),
2550
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Units" }),
2551
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Amount" })
2552
+ ] }) }),
2553
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: usage.chargesUsage.map((charge, index) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2554
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: charge.charge?.invoiceDisplayName || charge.feature?.name || charge.feature?.code || "Usage" }),
2555
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: charge.units }),
2556
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: money(charge.amountCents, usage.currency, locale) })
2557
+ ] }, `${charge.id}-${index}`)) })
2558
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-empty", children: "No usage recorded for this billing period." })
2559
+ ] })
2560
+ ] });
2561
+ }
2562
+ function Invoices(props) {
2563
+ const { client, locale, reconnect, onError } = props;
2564
+ const [search, setSearch] = React.useState("");
2565
+ const [filter, setFilter] = React.useState({ page: 1, searchTerm: "" });
2566
+ const { data, error, retry } = useQuery(client, invoicesQuery, filter, onError);
2567
+ const result = data?.customerPortalInvoices;
2568
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Invoice history", children: [
2569
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Invoice history" }),
2570
+ /* @__PURE__ */ jsxRuntime.jsxs(
2571
+ "form",
2572
+ {
2573
+ className: "nzp-actions",
2574
+ onSubmit: (event) => {
2575
+ event.preventDefault();
2576
+ setFilter({ page: 1, searchTerm: search.trim() });
2577
+ },
2578
+ children: [
2579
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
2580
+ "Search invoices",
2581
+ /* @__PURE__ */ jsxRuntime.jsx(
2582
+ "input",
2583
+ {
2584
+ type: "search",
2585
+ value: search,
2586
+ onChange: (event) => setSearch(event.target.value)
2587
+ }
2588
+ )
2589
+ ] }),
2590
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", children: "Search" })
2591
+ ]
2592
+ }
2593
+ ),
2594
+ error ? /* @__PURE__ */ jsxRuntime.jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsxRuntime.jsx(Loading, {}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2595
+ !result.collection.length ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-empty", children: filter.searchTerm ? "No invoices match your search." : "No invoices yet." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-table", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
2596
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2597
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Invoice" }),
2598
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Date" }),
2599
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Amount" }),
2600
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "Status" }),
2601
+ /* @__PURE__ */ jsxRuntime.jsx("th", { children: "PDF" })
2602
+ ] }) }),
2603
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: result.collection.map((invoice) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2604
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: invoice.number }),
2605
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: date(invoice.issuingDate, locale) }),
2606
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: money(
2607
+ invoice.totalAmountCents,
2608
+ invoice.currency,
2609
+ locale
2610
+ ) }),
2611
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: invoice.paymentOverdue ? "Overdue" : invoice.paymentStatus === "succeeded" ? "Paid" : invoice.paymentDisputeLostAt ? "Disputed" : "To pay" }),
2612
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsx(InvoiceDownload, { ...props, invoice }) })
2613
+ ] }, invoice.id)) })
2614
+ ] }) }),
2615
+ /* @__PURE__ */ jsxRuntime.jsx(
2616
+ Pager,
2617
+ {
2618
+ page: filter.page,
2619
+ total: result.metadata.totalPages,
2620
+ setPage: (page) => setFilter({ ...filter, page })
2621
+ }
2622
+ )
2623
+ ] })
2624
+ ] });
2625
+ }
2626
+ function InvoiceDownload({
2627
+ client,
2628
+ invoice,
2629
+ onError,
2630
+ reconnect
2631
+ }) {
2632
+ const [url, setUrl] = React.useState("");
2633
+ const [generating, setGenerating] = React.useState(false);
2634
+ const action = useAction(onError);
2635
+ const download = () => action.run(async (signal) => {
2636
+ const data = await client.request(downloadInvoiceMutation, { input: { id: invoice.id } }, signal);
2637
+ if (signal.aborted) return;
2638
+ const file = data.downloadCustomerPortalInvoice.fileUrl;
2639
+ if (file) {
2640
+ setUrl(invoiceDownloadUrl(file));
2641
+ setGenerating(false);
2642
+ } else setGenerating(true);
2643
+ });
2644
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2645
+ url ? /* @__PURE__ */ jsxRuntime.jsx(
2646
+ "a",
2647
+ {
2648
+ className: "nzp-link",
2649
+ href: url,
2650
+ target: "_blank",
2651
+ rel: "noopener noreferrer",
2652
+ referrerPolicy: "no-referrer",
2653
+ children: "Open PDF"
2654
+ }
2655
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2656
+ "button",
2657
+ {
2658
+ type: "button",
2659
+ disabled: action.pending,
2660
+ "aria-label": `Download invoice ${invoice.number}`,
2661
+ onClick: download,
2662
+ children: action.pending ? "Preparing\u2026" : generating ? "Check PDF" : "Download"
2663
+ }
2664
+ ),
2665
+ generating && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-muted", role: "status", children: "PDF is being generated. Check again shortly." }),
2666
+ action.error && /* @__PURE__ */ jsxRuntime.jsx(Failure, { error: action.error, reconnect })
2667
+ ] });
2668
+ }
2669
+ var customerFields = [
2670
+ ["name", "Account name"],
2671
+ ["firstname", "First name"],
2672
+ ["lastname", "Last name"],
2673
+ ["email", "Email"],
2674
+ ["legalName", "Legal name"],
2675
+ ["taxIdentificationNumber", "Tax ID"],
2676
+ ["addressLine1", "Address line 1"],
2677
+ ["addressLine2", "Address line 2"],
2678
+ ["city", "City"],
2679
+ ["state", "State / province"],
2680
+ ["zipcode", "Postal code"],
2681
+ ["country", "Country code"]
2682
+ ];
2683
+ function CustomerForm({
2684
+ client,
2685
+ customer,
2686
+ reconnect,
2687
+ onError,
2688
+ onSaved,
2689
+ onCancel
2690
+ }) {
2691
+ const [values, setValues] = React.useState(
2692
+ () => Object.fromEntries(
2693
+ customerFields.map(([key]) => [key, customer[key] ?? ""])
2694
+ )
2695
+ );
2696
+ const action = useAction(onError);
2697
+ const submit = (event) => {
2698
+ event.preventDefault();
2699
+ const input = Object.fromEntries(
2700
+ customerFields.filter(([key]) => values[key] !== (customer[key] ?? "")).map(([key]) => [
2701
+ key,
2702
+ key === "country" ? values[key].toUpperCase() || null : values[key]
2703
+ ])
2704
+ );
2705
+ void action.run(async (signal) => {
2706
+ await client.request(updateCustomerMutation, { input }, signal);
2707
+ if (!signal.aborted) onSaved();
1085
2708
  });
1086
- setIsProcessing(false);
1087
- if (result.error) {
1088
- const err = new Error(result.error.message ?? "Payment failed");
1089
- setError(err);
1090
- onError?.(err);
1091
- } else if (result.paymentIntent?.status === "succeeded") {
1092
- onSuccess?.(result.paymentIntent.id);
1093
- }
1094
- }
1095
- return /* @__PURE__ */ jsxRuntime.jsx(CheckoutContext.Provider, { value: { confirmPayment, isProcessing, error }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style, children: [
1096
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes nozle-skeleton-pulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }` }),
1097
- !isReady && /* @__PURE__ */ jsxRuntime.jsx(
1098
- "div",
2709
+ };
2710
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Edit billing information", children: [
2711
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Edit billing information" }),
2712
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: submit, children: [
2713
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-fields", children: customerFields.map(([key, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
2714
+ label,
2715
+ /* @__PURE__ */ jsxRuntime.jsx(
2716
+ "input",
2717
+ {
2718
+ type: key === "email" ? "email" : "text",
2719
+ value: values[key],
2720
+ disabled: action.pending,
2721
+ pattern: key === "country" ? "[A-Za-z]{2}" : void 0,
2722
+ maxLength: key === "country" ? 2 : 255,
2723
+ placeholder: key === "country" ? "US" : void 0,
2724
+ onChange: (event) => setValues({ ...values, [key]: event.target.value })
2725
+ }
2726
+ )
2727
+ ] }, key)) }),
2728
+ action.error && /* @__PURE__ */ jsxRuntime.jsx(Failure, { error: action.error, reconnect }),
2729
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-actions", children: [
2730
+ /* @__PURE__ */ jsxRuntime.jsx(
2731
+ "button",
2732
+ {
2733
+ type: "submit",
2734
+ className: "nzp-primary",
2735
+ disabled: action.pending,
2736
+ children: action.pending ? "Saving\u2026" : "Save billing information"
2737
+ }
2738
+ ),
2739
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: action.pending, onClick: onCancel, children: "Cancel" })
2740
+ ] })
2741
+ ] })
2742
+ ] });
2743
+ }
2744
+ function Wallets({
2745
+ client,
2746
+ locale,
2747
+ reconnect,
2748
+ onError,
2749
+ onWallet
2750
+ }) {
2751
+ const [page, setPage] = React.useState(1);
2752
+ const { data, error, retry } = useQuery(client, walletsQuery, { page }, onError);
2753
+ const result = data?.customerPortalWallets;
2754
+ if (result && !result.collection.length) return null;
2755
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Wallets", children: [
2756
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Wallets" }),
2757
+ error ? /* @__PURE__ */ jsxRuntime.jsx(Failure, { error, retry, reconnect }) : !result ? /* @__PURE__ */ jsxRuntime.jsx(Loading, {}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2758
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-stack", children: result.collection.map((wallet) => /* @__PURE__ */ jsxRuntime.jsxs("article", { className: "nzp-card", children: [
2759
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { children: wallet.name || "Prepaid credits" }),
2760
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-price", children: money(wallet.balanceCents, wallet.currency, locale) }),
2761
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nzp-row", children: [
2762
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "nzp-muted", children: [
2763
+ wallet.creditsBalance,
2764
+ " credits available \xB7",
2765
+ " ",
2766
+ wallet.consumedCredits,
2767
+ " consumed"
2768
+ ] }),
2769
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => onWallet(wallet), children: "Add credits" })
2770
+ ] })
2771
+ ] }, wallet.id)) }),
2772
+ /* @__PURE__ */ jsxRuntime.jsx(
2773
+ Pager,
2774
+ {
2775
+ page,
2776
+ total: result.metadata.totalPages,
2777
+ setPage
2778
+ }
2779
+ )
2780
+ ] })
2781
+ ] });
2782
+ }
2783
+ function WalletTopUp({
2784
+ client,
2785
+ locale,
2786
+ wallet,
2787
+ reconnect,
2788
+ onError,
2789
+ onCreated
2790
+ }) {
2791
+ const [credits, setCredits] = React.useState("");
2792
+ const action = useAction(onError);
2793
+ const digits = new Intl.NumberFormat(locale, {
2794
+ style: "currency",
2795
+ currency: wallet.currency
2796
+ }).resolvedOptions().maximumFractionDigits ?? 2;
2797
+ const amountCents = walletAmountCents(credits, wallet.rateAmount, digits);
2798
+ const valid = amountCents !== null && compareDecimalStrings(credits, "0") > 0 && (wallet.paidTopUpMinAmountCents == null || compareDecimalStrings(
2799
+ amountCents,
2800
+ String(wallet.paidTopUpMinAmountCents)
2801
+ ) >= 0) && (wallet.paidTopUpMaxAmountCents == null || compareDecimalStrings(
2802
+ amountCents,
2803
+ String(wallet.paidTopUpMaxAmountCents)
2804
+ ) <= 0);
2805
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Add credits", children: [
2806
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { children: [
2807
+ "Add credits \xB7 ",
2808
+ wallet.name || "Wallet"
2809
+ ] }),
2810
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nzp-muted", children: "A paid top-up creates a billing transaction. Credits become available after payment is processed." }),
2811
+ /* @__PURE__ */ jsxRuntime.jsxs(
2812
+ "form",
1099
2813
  {
1100
- "data-testid": "checkout-skeleton",
1101
- style: {
1102
- height: "200px",
1103
- borderRadius: "var(--nozle-radius, 0.5rem)",
1104
- background: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
1105
- backgroundSize: "200% 100%",
1106
- animation: "nozle-skeleton-pulse 1.5s ease-in-out infinite"
2814
+ onSubmit: (event) => {
2815
+ event.preventDefault();
2816
+ if (valid)
2817
+ void action.run(async (signal) => {
2818
+ await client.request(
2819
+ topUpMutation,
2820
+ { input: { walletId: wallet.id, paidCredits: credits } },
2821
+ signal
2822
+ );
2823
+ if (!signal.aborted) onCreated();
2824
+ });
2825
+ },
2826
+ children: [
2827
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-fields", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
2828
+ "Credits to add",
2829
+ /* @__PURE__ */ jsxRuntime.jsx(
2830
+ "input",
2831
+ {
2832
+ inputMode: "decimal",
2833
+ maxLength: 36,
2834
+ required: true,
2835
+ value: credits,
2836
+ disabled: action.pending,
2837
+ onChange: (event) => setCredits(event.target.value)
2838
+ }
2839
+ )
2840
+ ] }) }),
2841
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2842
+ amountCents !== null ? money(amountCents, wallet.currency, locale) : "\u2014",
2843
+ " ",
2844
+ "excluding tax"
2845
+ ] }),
2846
+ wallet.paidTopUpMinAmountCents != null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "nzp-muted", children: [
2847
+ "Minimum",
2848
+ " ",
2849
+ money(wallet.paidTopUpMinAmountCents, wallet.currency, locale)
2850
+ ] }),
2851
+ wallet.paidTopUpMaxAmountCents != null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "nzp-muted", children: [
2852
+ "Maximum",
2853
+ " ",
2854
+ money(wallet.paidTopUpMaxAmountCents, wallet.currency, locale)
2855
+ ] }),
2856
+ action.error && /* @__PURE__ */ jsxRuntime.jsx(Failure, { error: action.error, reconnect }),
2857
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nzp-actions", children: /* @__PURE__ */ jsxRuntime.jsx(
2858
+ "button",
2859
+ {
2860
+ type: "submit",
2861
+ className: "nzp-primary",
2862
+ disabled: !valid || action.pending,
2863
+ children: action.pending ? "Requesting\u2026" : "Confirm paid top-up"
2864
+ }
2865
+ ) })
2866
+ ]
2867
+ }
2868
+ )
2869
+ ] });
2870
+ }
2871
+ function usePlans() {
2872
+ const { client } = useBillingContext();
2873
+ const [plans, setPlans] = React.useState([]);
2874
+ const [isLoading, setIsLoading] = React.useState(true);
2875
+ React.useEffect(() => {
2876
+ let cancelled = false;
2877
+ if (!client) {
2878
+ setIsLoading(false);
2879
+ return;
2880
+ }
2881
+ void client.catalogFetch("/api/v1/plans").then(async (response) => {
2882
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
2883
+ const data = await response.json();
2884
+ if (!cancelled) setPlans(data.plans ?? []);
2885
+ }).finally(() => {
2886
+ if (!cancelled) setIsLoading(false);
2887
+ });
2888
+ return () => {
2889
+ cancelled = true;
2890
+ };
2891
+ }, [client]);
2892
+ return { plans, isLoading };
2893
+ }
2894
+ function useCheckout2() {
2895
+ const { createCheckout } = useBillingContext();
2896
+ const [isLoading, setIsLoading] = React.useState(false);
2897
+ const [error, setError] = React.useState(null);
2898
+ const [checkout, setCheckout] = React.useState(null);
2899
+ const fetchClientSecret = React.useCallback(
2900
+ async (planCode, returnUrl) => {
2901
+ setIsLoading(true);
2902
+ setError(null);
2903
+ try {
2904
+ if (!createCheckout) {
2905
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
1107
2906
  }
1108
- }
1109
- ),
1110
- /* @__PURE__ */ jsxRuntime.jsx(
1111
- reactStripeJs.PaymentElement,
1112
- {
1113
- onReady: () => {
1114
- setIsReady(true);
1115
- onReady?.();
2907
+ const result = await createCheckout({
2908
+ planCode,
2909
+ returnUrl: returnUrl ?? window.location.href
2910
+ });
2911
+ setCheckout(result);
2912
+ if ("client_secret" in result || "clientSecret" in result) {
2913
+ const clientSecret = result.client_secret ?? result.clientSecret;
2914
+ if (clientSecret) return clientSecret;
1116
2915
  }
2916
+ if ("url" in result && result.url) {
2917
+ navigateToCheckout(result.url);
2918
+ return null;
2919
+ }
2920
+ if ("type" in result && result.type === "hosted") {
2921
+ navigateToCheckout(result.payment_url);
2922
+ return null;
2923
+ }
2924
+ if ("type" in result && ["completed", "scheduled", "razorpay", "processing"].includes(result.type)) {
2925
+ return null;
2926
+ }
2927
+ throw new Error("Checkout response did not include a supported result type");
2928
+ } catch (cause) {
2929
+ const checkoutError = cause instanceof Error ? cause : new Error(String(cause));
2930
+ setError(checkoutError);
2931
+ throw checkoutError;
2932
+ } finally {
2933
+ setIsLoading(false);
1117
2934
  }
1118
- ),
1119
- error !== null && /* @__PURE__ */ jsxRuntime.jsx(
1120
- "p",
1121
- {
1122
- style: {
1123
- color: "var(--nozle-destructive, #dc2626)",
1124
- margin: "0.5rem 0 0"
1125
- },
1126
- children: error.message
1127
- }
1128
- ),
1129
- children ?? /* @__PURE__ */ jsxRuntime.jsx(
1130
- "button",
1131
- {
1132
- type: "button",
1133
- onClick: () => void confirmPayment(),
1134
- disabled: isProcessing || !stripe,
1135
- "aria-busy": isProcessing,
1136
- style: {
1137
- padding: "0.75rem 1.5rem",
1138
- borderRadius: "var(--nozle-radius, 0.5rem)",
1139
- border: "none",
1140
- background: "var(--nozle-primary, var(--primary))",
1141
- color: "var(--nozle-primary-foreground, var(--primary-foreground))",
1142
- cursor: isProcessing ? "not-allowed" : "pointer",
1143
- fontWeight: 500,
1144
- opacity: isProcessing ? 0.7 : 1,
1145
- marginTop: "1rem",
1146
- width: "100%"
1147
- },
1148
- children: isProcessing ? "Processing..." : submitLabel ?? "Pay now"
1149
- }
1150
- )
2935
+ },
2936
+ [createCheckout]
2937
+ );
2938
+ return { fetchClientSecret, checkout, isLoading, error };
2939
+ }
2940
+ var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
2941
+ function fmtPrice(cents, cur) {
2942
+ const s = SYMBOLS[cur] ?? cur + " ";
2943
+ const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
2944
+ return `${s}${val}`;
2945
+ }
2946
+ function isEnterprise(p) {
2947
+ return /enterprise|custom/i.test(p.name + p.code);
2948
+ }
2949
+ function v(name, fb) {
2950
+ return `var(${name}, ${fb})`;
2951
+ }
2952
+ var C = {
2953
+ bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
2954
+ cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
2955
+ hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
2956
+ border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
2957
+ radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
2958
+ text: v("--nozle-foreground", "#111827"),
2959
+ muted: v("--nozle-muted-foreground", "#6b7280"),
2960
+ mutedBg: v("--nozle-muted", "#f3f4f6"),
2961
+ hlFg: v("--nozle-primary-foreground", "#ffffff")
2962
+ };
2963
+ function Check() {
2964
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
2965
+ }
2966
+ function Toggle({ isAnnual, onChange }) {
2967
+ const pill = {
2968
+ display: "inline-flex",
2969
+ borderRadius: "9999px",
2970
+ padding: "4px",
2971
+ background: C.mutedBg,
2972
+ gap: "2px"
2973
+ };
2974
+ const tab = (active) => ({
2975
+ padding: "8px 20px",
2976
+ borderRadius: "9999px",
2977
+ border: "none",
2978
+ background: active ? C.cardBg : "transparent",
2979
+ color: active ? C.text : C.muted,
2980
+ fontWeight: active ? 600 : 400,
2981
+ fontSize: "0.875rem",
2982
+ cursor: "pointer",
2983
+ boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
2984
+ transition: "all 0.15s ease"
2985
+ });
2986
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pill, children: [
2987
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
2988
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
2989
+ "Annual",
2990
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2991
+ marginLeft: "6px",
2992
+ fontSize: "0.7rem",
2993
+ fontWeight: 600,
2994
+ color: isAnnual ? C.hl : C.muted
2995
+ }, children: "Save 20%" })
2996
+ ] })
1151
2997
  ] }) });
1152
2998
  }
1153
- function StripeCheckout({
1154
- clientSecret,
1155
- publishableKey,
1156
- stripeAccount,
1157
- returnUrl,
1158
- submitLabel,
1159
- onSuccess,
1160
- onComplete,
1161
- onError,
1162
- onReady,
1163
- className,
1164
- style,
1165
- children
2999
+ function Card({
3000
+ plan,
3001
+ features,
3002
+ isCurrent,
3003
+ isHighlighted,
3004
+ isEnt,
3005
+ enterpriseEmail,
3006
+ onSelect
1166
3007
  }) {
1167
- const stripePromise = React.useMemo(
1168
- () => stripeJs.loadStripe(publishableKey, stripeAccount ? { stripeAccount } : void 0),
1169
- [publishableKey, stripeAccount]
1170
- );
1171
- const appearance = React.useMemo(() => buildStripeAppearance(), []);
1172
- const embeddedOptions = React.useMemo(
1173
- () => ({ clientSecret, onComplete }),
1174
- [clientSecret, onComplete]
3008
+ const [hover, setHover] = React.useState(false);
3009
+ const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
3010
+ const interval = plan.interval === "yearly" ? "/year" : "/month";
3011
+ const handleClick = () => {
3012
+ if (isCurrent) return;
3013
+ if (isEnt && enterpriseEmail) {
3014
+ window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
3015
+ return;
3016
+ }
3017
+ onSelect?.();
3018
+ };
3019
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3020
+ "div",
3021
+ {
3022
+ onMouseEnter: () => setHover(true),
3023
+ onMouseLeave: () => setHover(false),
3024
+ style: {
3025
+ position: "relative",
3026
+ display: "flex",
3027
+ flexDirection: "column",
3028
+ background: C.cardBg,
3029
+ border: `1px solid ${isHighlighted ? C.hl : C.border}`,
3030
+ borderTopWidth: isHighlighted ? "3px" : "1px",
3031
+ borderRadius: C.radius,
3032
+ padding: "2rem",
3033
+ transition: "box-shadow 0.2s ease, transform 0.2s ease",
3034
+ boxShadow: shadow,
3035
+ transform: hover && !isCurrent ? "translateY(-2px)" : "none"
3036
+ },
3037
+ children: [
3038
+ isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
3039
+ position: "absolute",
3040
+ top: "-1px",
3041
+ left: "50%",
3042
+ transform: "translate(-50%, -50%)",
3043
+ background: C.hl,
3044
+ color: C.hlFg,
3045
+ fontSize: "0.7rem",
3046
+ fontWeight: 700,
3047
+ padding: "4px 12px",
3048
+ borderRadius: "9999px",
3049
+ letterSpacing: "0.025em",
3050
+ textTransform: "uppercase",
3051
+ whiteSpace: "nowrap"
3052
+ }, children: isCurrent ? "Current Plan" : "Most Popular" }),
3053
+ isCurrent && !isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
3054
+ position: "absolute",
3055
+ top: "0",
3056
+ left: "50%",
3057
+ transform: "translate(-50%, -50%)",
3058
+ background: C.mutedBg,
3059
+ color: C.muted,
3060
+ fontSize: "0.7rem",
3061
+ fontWeight: 700,
3062
+ padding: "4px 12px",
3063
+ borderRadius: "9999px",
3064
+ letterSpacing: "0.025em",
3065
+ textTransform: "uppercase",
3066
+ whiteSpace: "nowrap"
3067
+ }, children: "Current Plan" }),
3068
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
3069
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: {
3070
+ fontSize: "1.25rem",
3071
+ fontWeight: 700,
3072
+ color: C.text,
3073
+ margin: 0,
3074
+ lineHeight: 1.3
3075
+ }, children: plan.name }),
3076
+ plan.description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
3077
+ fontSize: "0.875rem",
3078
+ color: C.muted,
3079
+ margin: "6px 0 0",
3080
+ lineHeight: 1.5
3081
+ }, children: plan.description })
3082
+ ] }),
3083
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
3084
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
3085
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
3086
+ ] }) }),
3087
+ features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { style: {
3088
+ listStyle: "none",
3089
+ padding: 0,
3090
+ margin: "0 0 2rem",
3091
+ display: "flex",
3092
+ flexDirection: "column",
3093
+ gap: "0.75rem",
3094
+ flex: 1
3095
+ }, children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { style: {
3096
+ display: "flex",
3097
+ alignItems: "flex-start",
3098
+ gap: "10px",
3099
+ fontSize: "0.875rem",
3100
+ color: C.text,
3101
+ lineHeight: 1.5
3102
+ }, children: [
3103
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
3104
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feat })
3105
+ ] }, feat)) }),
3106
+ (!features || features.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
3107
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
3108
+ CTAButton,
3109
+ {
3110
+ onClick: handleClick,
3111
+ disabled: isCurrent,
3112
+ variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
3113
+ children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
3114
+ }
3115
+ )
3116
+ ]
3117
+ }
1175
3118
  );
1176
- if (clientSecret.startsWith("cs_")) {
1177
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: /* @__PURE__ */ jsxRuntime.jsx(
1178
- reactStripeJs.EmbeddedCheckoutProvider,
1179
- {
1180
- stripe: stripePromise,
1181
- options: embeddedOptions,
1182
- children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckout, {})
1183
- }
1184
- ) });
1185
- }
1186
- const options = { clientSecret, appearance };
1187
- return /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsxRuntime.jsx(
1188
- CheckoutInner,
3119
+ }
3120
+ function CTAButton({
3121
+ children,
3122
+ onClick,
3123
+ disabled,
3124
+ variant
3125
+ }) {
3126
+ const [hover, setHover] = React.useState(false);
3127
+ const base = {
3128
+ width: "100%",
3129
+ padding: "12px 24px",
3130
+ borderRadius: "10px",
3131
+ fontSize: "0.9375rem",
3132
+ fontWeight: 600,
3133
+ cursor: disabled ? "default" : "pointer",
3134
+ border: "none",
3135
+ transition: "all 0.15s ease",
3136
+ lineHeight: 1.4
3137
+ };
3138
+ const variants = {
3139
+ primary: {
3140
+ background: hover && !disabled ? "#4f46e5" : C.hl,
3141
+ color: C.hlFg
3142
+ },
3143
+ secondary: {
3144
+ background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
3145
+ color: C.text,
3146
+ border: `1px solid ${C.border}`
3147
+ },
3148
+ outline: {
3149
+ background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
3150
+ color: C.hl,
3151
+ border: `1.5px solid ${C.hl}`
3152
+ },
3153
+ current: {
3154
+ background: C.mutedBg,
3155
+ color: C.muted
3156
+ }
3157
+ };
3158
+ return /* @__PURE__ */ jsxRuntime.jsx(
3159
+ "button",
1189
3160
  {
1190
- submitLabel,
1191
- onSuccess,
1192
- onError,
1193
- onReady,
1194
- className,
1195
- style,
1196
- returnUrl,
3161
+ onClick: disabled ? void 0 : onClick,
3162
+ disabled,
3163
+ onMouseEnter: () => setHover(true),
3164
+ onMouseLeave: () => setHover(false),
3165
+ style: { ...base, ...variants[variant] },
1197
3166
  children
1198
3167
  }
1199
- ) }, clientSecret);
3168
+ );
1200
3169
  }
1201
- function ProviderCheckout(props) {
3170
+ function Skeleton() {
3171
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
3172
+ display: "grid",
3173
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3174
+ gap: "1.5rem"
3175
+ }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
3176
+ background: C.cardBg,
3177
+ border: `1px solid ${C.border}`,
3178
+ borderRadius: C.radius,
3179
+ padding: "2rem",
3180
+ height: "380px"
3181
+ }, children: [
3182
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
3183
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
3184
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
3185
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
3186
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
3187
+ ] }, i)) });
3188
+ }
3189
+ function PricingTable({
3190
+ currentPlanCode,
3191
+ plans: propPlans,
3192
+ features,
3193
+ onSelect,
3194
+ returnUrl,
3195
+ onStripeClientSecret,
3196
+ onCheckoutResult,
3197
+ onCheckoutError,
3198
+ showToggle = true,
3199
+ highlightPlan,
3200
+ className,
3201
+ enterpriseEmail
3202
+ }) {
1202
3203
  const billing = useOptionalBillingContext();
1203
- const [busy, setBusy] = React.useState(false);
1204
- const [error, setError] = React.useState(null);
3204
+ const client = billing?.client;
3205
+ const [plans, setPlans] = React.useState(propPlans ?? []);
3206
+ const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
3207
+ const [isAnnual, setIsAnnual] = React.useState(false);
3208
+ const [loading, setLoading] = React.useState(!propPlans);
1205
3209
  const [processing, setProcessing] = React.useState(false);
1206
- const pay = async () => {
1207
- if (busy) return;
1208
- setBusy(true);
1209
- setError(null);
3210
+ const [checkoutError, setCheckoutError] = React.useState(null);
3211
+ React.useEffect(() => {
3212
+ if (propPlans) {
3213
+ setPlans(propPlans);
3214
+ return;
3215
+ }
3216
+ if (!client) {
3217
+ setLoading(false);
3218
+ setCheckoutError("BillingProvider with a publishableKey is required to load plans");
3219
+ return;
3220
+ }
3221
+ let cancelled = false;
3222
+ client.catalogFetch("/api/v1/plans").then(async (res) => {
3223
+ if (!cancelled && res.ok) {
3224
+ const data = await res.json();
3225
+ setPlans(data.plans || []);
3226
+ }
3227
+ }).finally(() => {
3228
+ if (!cancelled) setLoading(false);
3229
+ });
3230
+ return () => {
3231
+ cancelled = true;
3232
+ };
3233
+ }, [client, propPlans]);
3234
+ React.useEffect(() => {
3235
+ if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
3236
+ }, [currentPlanCode]);
3237
+ async function selectPlan(plan) {
3238
+ setCheckoutError(null);
3239
+ if (onSelect) {
3240
+ onSelect(plan);
3241
+ return;
3242
+ }
1210
3243
  try {
1211
- await handleCheckoutResult(props.checkout, {
3244
+ if (!billing?.createCheckout) {
3245
+ throw new Error("BillingProvider createCheckout callback is required for checkout");
3246
+ }
3247
+ const result = await billing.createCheckout({
3248
+ planCode: plan.code,
3249
+ returnUrl: returnUrl ?? window.location.href
3250
+ });
3251
+ onCheckoutResult?.(result);
3252
+ await handleCheckoutResult(result, {
3253
+ onStripeClientSecret,
1212
3254
  verifyCheckout: billing?.verifyCheckout,
1213
3255
  getCheckoutStatus: billing?.getCheckoutStatus,
1214
- onSuccess: (id) => {
1215
- setProcessing(false);
1216
- props.onSuccess?.(id);
1217
- props.onComplete?.();
1218
- },
1219
- onComplete: () => {
1220
- setProcessing(false);
1221
- props.onComplete?.();
1222
- },
1223
- onProcessing: (status) => {
1224
- setProcessing(true);
1225
- props.onProcessing?.(status);
1226
- }
3256
+ onProcessing: () => setProcessing(true),
3257
+ onSuccess: () => setProcessing(false),
3258
+ onComplete: () => setProcessing(false)
1227
3259
  });
1228
3260
  } catch (cause) {
1229
- const failure = cause instanceof Error ? cause : new Error("Checkout failed");
1230
- setError(failure);
1231
- props.onError?.(failure);
1232
- } finally {
1233
- setBusy(false);
3261
+ const error = cause instanceof Error ? cause : new Error("Checkout failed");
3262
+ setCheckoutError(error.message);
3263
+ onCheckoutError?.(error);
1234
3264
  }
1235
- };
1236
- return /* @__PURE__ */ jsxRuntime.jsx(
1237
- CheckoutContext.Provider,
3265
+ }
3266
+ const hasAnnual = plans.some((p) => p.interval === "yearly");
3267
+ const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
3268
+ if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, {});
3269
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: {
3270
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
3271
+ color: C.text,
3272
+ background: C.bg
3273
+ }, children: [
3274
+ showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
3275
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
3276
+ checkoutError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: checkoutError }),
3277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
3278
+ display: "grid",
3279
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
3280
+ gap: "1.5rem"
3281
+ }, children: filtered.map((plan, i) => /* @__PURE__ */ jsxRuntime.jsx(
3282
+ Card,
3283
+ {
3284
+ plan,
3285
+ features: features?.[i],
3286
+ isCurrent: currentPlan === plan.code,
3287
+ isHighlighted: highlightPlan === plan.code,
3288
+ isEnt: isEnterprise(plan),
3289
+ enterpriseEmail,
3290
+ onSelect: () => void selectPlan(plan)
3291
+ },
3292
+ plan.code
3293
+ )) })
3294
+ ] });
3295
+ }
3296
+ function PlanCard({
3297
+ id,
3298
+ name,
3299
+ monthlyPrice,
3300
+ annualPrice,
3301
+ features,
3302
+ description,
3303
+ isAnnual,
3304
+ isCurrent,
3305
+ returnUrl,
3306
+ onSelect
3307
+ }) {
3308
+ const price = isAnnual ? annualPrice : monthlyPrice;
3309
+ const interval = isAnnual ? "year" : "month";
3310
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3311
+ "div",
1238
3312
  {
1239
- value: { confirmPayment: pay, isProcessing: busy, error },
1240
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: props.className, children: [
3313
+ "data-testid": `plan-card-${id}`,
3314
+ "aria-current": isCurrent ? "true" : void 0,
3315
+ style: {
3316
+ background: "var(--nozle-background, var(--background))",
3317
+ border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
3318
+ borderRadius: "var(--nozle-radius, 0.5rem)",
3319
+ padding: "1.5rem",
3320
+ display: "flex",
3321
+ flexDirection: "column",
3322
+ gap: "1rem"
3323
+ },
3324
+ className: isCurrent ? "border-primary" : "",
3325
+ children: [
3326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3327
+ /* @__PURE__ */ jsxRuntime.jsx(
3328
+ "h3",
3329
+ {
3330
+ style: {
3331
+ fontSize: "1.25rem",
3332
+ fontWeight: 600,
3333
+ color: "var(--nozle-foreground, var(--foreground))",
3334
+ margin: 0
3335
+ },
3336
+ children: name
3337
+ }
3338
+ ),
3339
+ description && /* @__PURE__ */ jsxRuntime.jsx(
3340
+ "p",
3341
+ {
3342
+ style: {
3343
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3344
+ fontSize: "0.875rem",
3345
+ margin: "0.25rem 0 0"
3346
+ },
3347
+ children: description
3348
+ }
3349
+ )
3350
+ ] }),
3351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3352
+ /* @__PURE__ */ jsxRuntime.jsxs(
3353
+ "span",
3354
+ {
3355
+ style: {
3356
+ fontSize: "2rem",
3357
+ fontWeight: 700,
3358
+ color: "var(--nozle-foreground, var(--foreground))"
3359
+ },
3360
+ children: [
3361
+ "$",
3362
+ price
3363
+ ]
3364
+ }
3365
+ ),
3366
+ /* @__PURE__ */ jsxRuntime.jsxs(
3367
+ "span",
3368
+ {
3369
+ style: {
3370
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3371
+ marginLeft: "0.25rem"
3372
+ },
3373
+ children: [
3374
+ "/",
3375
+ interval
3376
+ ]
3377
+ }
3378
+ )
3379
+ ] }),
1241
3380
  /* @__PURE__ */ jsxRuntime.jsx(
1242
- "button",
3381
+ "ul",
1243
3382
  {
1244
- type: "button",
1245
- disabled: busy,
1246
- "aria-busy": busy,
1247
- onClick: () => void pay(),
1248
- children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
3383
+ style: {
3384
+ listStyle: "none",
3385
+ padding: 0,
3386
+ margin: 0,
3387
+ display: "flex",
3388
+ flexDirection: "column",
3389
+ gap: "0.5rem",
3390
+ flex: 1
3391
+ },
3392
+ children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs(
3393
+ "li",
3394
+ {
3395
+ style: {
3396
+ display: "flex",
3397
+ alignItems: "center",
3398
+ gap: "0.5rem",
3399
+ color: "var(--nozle-foreground, var(--foreground))",
3400
+ fontSize: "0.875rem"
3401
+ },
3402
+ children: [
3403
+ /* @__PURE__ */ jsxRuntime.jsx(
3404
+ "span",
3405
+ {
3406
+ style: { color: "var(--nozle-primary, var(--primary))" },
3407
+ "aria-hidden": "true",
3408
+ children: "\u2713"
3409
+ }
3410
+ ),
3411
+ feature
3412
+ ]
3413
+ },
3414
+ feature
3415
+ ))
1249
3416
  }
1250
3417
  ),
1251
- processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1252
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error.message })
1253
- ] })
3418
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
3419
+ "button",
3420
+ {
3421
+ onClick: () => onSelect?.({
3422
+ planCode: id,
3423
+ returnUrl: returnUrl ?? window.location.href
3424
+ }),
3425
+ style: {
3426
+ background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
3427
+ color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
3428
+ border: "none",
3429
+ borderRadius: "var(--nozle-radius, 0.5rem)",
3430
+ padding: "0.75rem 1.5rem",
3431
+ cursor: isCurrent ? "default" : "pointer",
3432
+ fontWeight: 500,
3433
+ width: "100%"
3434
+ },
3435
+ disabled: isCurrent,
3436
+ "aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
3437
+ children: isCurrent ? "Current Plan" : "Get Started"
3438
+ }
3439
+ )
3440
+ ]
1254
3441
  }
1255
3442
  );
1256
3443
  }
1257
- function Checkout(props) {
1258
- return "checkout" in props ? /* @__PURE__ */ jsxRuntime.jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(StripeCheckout, { ...props });
1259
- }
1260
- function UpgradeModal({
1261
- isOpen,
1262
- planCode,
1263
- returnUrl,
1264
- preview,
1265
- onStripeClientSecret,
1266
- onCheckoutStarted,
1267
- onCompleted,
1268
- onScheduled,
1269
- onConfirm,
1270
- onCancel,
1271
- onError
3444
+ function PlanComparison({
3445
+ plans,
3446
+ features
1272
3447
  }) {
1273
- const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
1274
- const [processing, setProcessing] = React.useState(false);
1275
- const [error, setError] = React.useState(null);
1276
- const [confirming, setConfirming] = React.useState(false);
1277
- async function handleConfirm() {
1278
- setConfirming(true);
1279
- setError(null);
1280
- try {
1281
- if (!createCheckout) {
1282
- throw new Error("BillingProvider createCheckout callback is required for checkout");
1283
- }
1284
- const result = await createCheckout({
1285
- planCode,
1286
- returnUrl: returnUrl ?? window.location.href
1287
- });
1288
- await handleCheckoutResult(result, {
1289
- verifyCheckout,
1290
- getCheckoutStatus,
1291
- onSuccess: () => {
1292
- setProcessing(false);
1293
- onCompleted?.();
1294
- },
1295
- onProcessing: () => setProcessing(true),
1296
- onStripeClientSecret: (clientSecret) => {
1297
- onStripeClientSecret?.(clientSecret);
1298
- onCheckoutStarted?.();
1299
- },
1300
- onComplete: () => {
1301
- setProcessing(false);
1302
- onCompleted?.();
1303
- },
1304
- onScheduled: () => onScheduled?.()
1305
- });
1306
- onConfirm?.();
1307
- } catch (cause) {
1308
- const checkoutError = cause instanceof Error ? cause : new Error("Checkout failed");
1309
- setError(checkoutError.message);
1310
- onError?.(checkoutError);
1311
- } finally {
1312
- setConfirming(false);
1313
- }
1314
- }
1315
- if (!isOpen) return null;
1316
3448
  return /* @__PURE__ */ jsxRuntime.jsx(
1317
3449
  "div",
1318
3450
  {
1319
3451
  style: {
1320
- position: "fixed",
1321
- inset: 0,
1322
- background: "oklch(0 0 0 / 0.5)",
1323
- display: "flex",
1324
- alignItems: "center",
1325
- justifyContent: "center",
1326
- zIndex: 1e3
3452
+ overflowX: "auto",
3453
+ background: "var(--nozle-background, var(--background))"
1327
3454
  },
1328
- role: "dialog",
1329
- "aria-modal": "true",
1330
- "aria-label": "Change plan",
1331
3455
  children: /* @__PURE__ */ jsxRuntime.jsxs(
1332
- "div",
3456
+ "table",
1333
3457
  {
1334
3458
  style: {
1335
- background: "var(--nozle-background, var(--background))",
1336
- color: "var(--nozle-foreground, var(--foreground))",
1337
- border: "1px solid var(--nozle-border, var(--border))",
1338
- borderRadius: "var(--nozle-radius, 0.5rem)",
1339
- padding: "2rem",
1340
- maxWidth: "28rem",
1341
3459
  width: "100%",
1342
- boxShadow: "var(--nozle-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1))"
3460
+ borderCollapse: "collapse",
3461
+ fontSize: "0.875rem",
3462
+ color: "var(--nozle-foreground, var(--foreground))"
1343
3463
  },
1344
3464
  children: [
1345
- /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" }, children: "Confirm plan change" }),
1346
- processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1347
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", style: { color: "oklch(0.6 0.2 25)" }, children: error }),
1348
- preview && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1349
- /* @__PURE__ */ jsxRuntime.jsxs(
1350
- "div",
3465
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
3466
+ /* @__PURE__ */ jsxRuntime.jsx(
3467
+ "th",
1351
3468
  {
1352
3469
  style: {
1353
- background: "var(--nozle-muted, var(--muted))",
1354
- borderRadius: "var(--nozle-radius, 0.5rem)",
1355
- padding: "1rem",
1356
- display: "flex",
1357
- flexDirection: "column",
1358
- gap: "0.5rem"
3470
+ padding: "0.75rem 1rem",
3471
+ textAlign: "left",
3472
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
3473
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))",
3474
+ fontWeight: 500
1359
3475
  },
1360
- children: [
1361
- preview.credit > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1362
- "Credits applied: -$",
1363
- preview.credit.toFixed(2)
1364
- ] }),
1365
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1366
- "New plan charge: $",
1367
- preview.debit.toFixed(2)
1368
- ] }),
1369
- /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
1370
- "Due today: $",
1371
- preview.net.toFixed(2)
1372
- ] })
1373
- ]
3476
+ children: "Feature"
1374
3477
  }
1375
3478
  ),
1376
- /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.875rem" }, children: [
1377
- "Next billing date: ",
1378
- new Date(preview.nextBillingDate).toLocaleDateString(),
1379
- "."
1380
- ] })
1381
- ] }),
1382
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.75rem", justifyContent: "flex-end" }, children: [
1383
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onCancel, disabled: confirming, children: "Cancel" }),
1384
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void handleConfirm(), disabled: confirming, children: confirming ? "Starting checkout..." : "Confirm" })
1385
- ] })
3479
+ plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
3480
+ "th",
3481
+ {
3482
+ style: {
3483
+ padding: "0.75rem 1rem",
3484
+ textAlign: "center",
3485
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
3486
+ fontWeight: 600,
3487
+ color: "var(--nozle-foreground, var(--foreground))"
3488
+ },
3489
+ children: plan.name
3490
+ },
3491
+ plan.id
3492
+ ))
3493
+ ] }) }),
3494
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
3495
+ /* @__PURE__ */ jsxRuntime.jsx(
3496
+ "td",
3497
+ {
3498
+ style: {
3499
+ padding: "0.75rem 1rem",
3500
+ borderBottom: "1px solid var(--nozle-border, var(--border))",
3501
+ color: "var(--nozle-foreground, var(--foreground))"
3502
+ },
3503
+ children: feature.label
3504
+ }
3505
+ ),
3506
+ plans.map((plan) => {
3507
+ const value = feature.values[plan.id];
3508
+ return /* @__PURE__ */ jsxRuntime.jsx(
3509
+ "td",
3510
+ {
3511
+ style: {
3512
+ padding: "0.75rem 1rem",
3513
+ textAlign: "center",
3514
+ borderBottom: "1px solid var(--nozle-border, var(--border))"
3515
+ },
3516
+ children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsxRuntime.jsx(
3517
+ "span",
3518
+ {
3519
+ style: {
3520
+ color: "var(--nozle-primary, var(--primary))"
3521
+ },
3522
+ "aria-label": "Included",
3523
+ children: "\u2713"
3524
+ }
3525
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
3526
+ "span",
3527
+ {
3528
+ style: {
3529
+ color: "var(--nozle-muted-foreground, var(--muted-foreground))"
3530
+ },
3531
+ "aria-label": "Not included",
3532
+ children: "\u2715"
3533
+ }
3534
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: value ?? "\u2014" })
3535
+ },
3536
+ plan.id
3537
+ );
3538
+ })
3539
+ ] }, feature.key)) })
1386
3540
  ]
1387
3541
  }
1388
3542
  )
@@ -1392,6 +3546,9 @@ function UpgradeModal({
1392
3546
  function UpgradeButton({
1393
3547
  planCode,
1394
3548
  returnUrl,
3549
+ subscriptionId,
3550
+ quoteId,
3551
+ idempotencyKey,
1395
3552
  preview,
1396
3553
  label = "Upgrade",
1397
3554
  className,
@@ -1429,6 +3586,9 @@ function UpgradeButton({
1429
3586
  isOpen,
1430
3587
  planCode,
1431
3588
  returnUrl,
3589
+ subscriptionId,
3590
+ quoteId,
3591
+ idempotencyKey,
1432
3592
  preview,
1433
3593
  onStripeClientSecret,
1434
3594
  onCheckoutStarted: () => {
@@ -1483,10 +3643,10 @@ function PlanGate({
1483
3643
  }
1484
3644
  function UpgradePrompt({
1485
3645
  text = "Upgrade to access this feature",
1486
- planName,
3646
+ planName: planName2,
1487
3647
  onUpgrade
1488
3648
  }) {
1489
- const displayText = planName ? `Upgrade to ${planName} to access this feature` : text;
3649
+ const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text;
1490
3650
  function handleUpgrade() {
1491
3651
  if (onUpgrade) {
1492
3652
  onUpgrade();
@@ -1868,7 +4028,10 @@ function PaymentMethodDisplay({
1868
4028
  }
1869
4029
 
1870
4030
  exports.BillingContext = BillingContext;
4031
+ exports.BillingPortal = BillingPortal;
4032
+ exports.BillingPortalError = BillingPortalError;
1871
4033
  exports.BillingProvider = BillingProvider;
4034
+ exports.CancellationControl = CancellationControl;
1872
4035
  exports.Checkout = Checkout;
1873
4036
  exports.CheckoutButton = CheckoutButton;
1874
4037
  exports.FeatureGate = FeatureGate;
@@ -1876,6 +4039,7 @@ exports.LockedOverlay = LockedOverlay;
1876
4039
  exports.PaymentMethodDisplay = PaymentMethodDisplay;
1877
4040
  exports.PlanBadge = PlanBadge;
1878
4041
  exports.PlanCard = PlanCard;
4042
+ exports.PlanChangeControl = PlanChangeControl;
1879
4043
  exports.PlanComparison = PlanComparison;
1880
4044
  exports.PlanGate = PlanGate;
1881
4045
  exports.PricingTable = PricingTable;
@@ -1888,8 +4052,8 @@ exports.UsageGate = UsageGate;
1888
4052
  exports.UsageMeter = UsageMeter;
1889
4053
  exports.getUsageMeterColor = getUsageMeterColor;
1890
4054
  exports.useBillingContext = useBillingContext;
1891
- exports.useCheckout = useCheckout2;
1892
- exports.useCheckoutSession = useCheckout;
4055
+ exports.useCheckout = useCheckout;
4056
+ exports.useCheckoutSession = useCheckout2;
1893
4057
  exports.useNozleClient = useNozleClient;
1894
4058
  exports.useOptionalBillingContext = useOptionalBillingContext;
1895
4059
  exports.usePlans = usePlans;