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