@nozle-js/react 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -7
- package/dist/index.cjs +3103 -939
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +220 -16
- package/dist/index.d.ts +220 -16
- package/dist/index.js +3100 -940
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,714 @@
|
|
|
1
|
-
import React, { createContext,
|
|
2
|
-
import { jsxs, Fragment
|
|
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/
|
|
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
|
-
|
|
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,
|
|
@@ -62,82 +765,6 @@ function useBillingContext() {
|
|
|
62
765
|
function useNozleClient() {
|
|
63
766
|
return useBillingContext().client;
|
|
64
767
|
}
|
|
65
|
-
function usePlans() {
|
|
66
|
-
const { client } = useBillingContext();
|
|
67
|
-
const [plans, setPlans] = useState([]);
|
|
68
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
let cancelled = false;
|
|
71
|
-
if (!client) {
|
|
72
|
-
setIsLoading(false);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
void client.catalogFetch("/api/v1/plans").then(async (response) => {
|
|
76
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
77
|
-
const data = await response.json();
|
|
78
|
-
if (!cancelled) setPlans(data.plans ?? []);
|
|
79
|
-
}).finally(() => {
|
|
80
|
-
if (!cancelled) setIsLoading(false);
|
|
81
|
-
});
|
|
82
|
-
return () => {
|
|
83
|
-
cancelled = true;
|
|
84
|
-
};
|
|
85
|
-
}, [client]);
|
|
86
|
-
return { plans, isLoading };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// src/components/billing/checkout-navigation.ts
|
|
90
|
-
function navigateToCheckout(url, location = window.location) {
|
|
91
|
-
location.assign(url);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/hooks/use-checkout.ts
|
|
95
|
-
function useCheckout() {
|
|
96
|
-
const { createCheckout } = useBillingContext();
|
|
97
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
98
|
-
const [error, setError] = useState(null);
|
|
99
|
-
const [checkout, setCheckout] = useState(null);
|
|
100
|
-
const fetchClientSecret = useCallback(
|
|
101
|
-
async (planCode, returnUrl) => {
|
|
102
|
-
setIsLoading(true);
|
|
103
|
-
setError(null);
|
|
104
|
-
try {
|
|
105
|
-
if (!createCheckout) {
|
|
106
|
-
throw new Error("BillingProvider createCheckout callback is required for checkout");
|
|
107
|
-
}
|
|
108
|
-
const result = await createCheckout({
|
|
109
|
-
planCode,
|
|
110
|
-
returnUrl: returnUrl ?? window.location.href
|
|
111
|
-
});
|
|
112
|
-
setCheckout(result);
|
|
113
|
-
if ("client_secret" in result || "clientSecret" in result) {
|
|
114
|
-
const clientSecret = result.client_secret ?? result.clientSecret;
|
|
115
|
-
if (clientSecret) return clientSecret;
|
|
116
|
-
}
|
|
117
|
-
if ("url" in result && result.url) {
|
|
118
|
-
navigateToCheckout(result.url);
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
if ("type" in result && result.type === "hosted") {
|
|
122
|
-
navigateToCheckout(result.payment_url);
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if ("type" in result && ["completed", "scheduled", "razorpay", "processing"].includes(result.type)) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
throw new Error("Checkout response did not include a supported result type");
|
|
129
|
-
} catch (cause) {
|
|
130
|
-
const checkoutError = cause instanceof Error ? cause : new Error(String(cause));
|
|
131
|
-
setError(checkoutError);
|
|
132
|
-
throw checkoutError;
|
|
133
|
-
} finally {
|
|
134
|
-
setIsLoading(false);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
[createCheckout]
|
|
138
|
-
);
|
|
139
|
-
return { fetchClientSecret, checkout, isLoading, error };
|
|
140
|
-
}
|
|
141
768
|
|
|
142
769
|
// src/components/billing/razorpay-checkout.ts
|
|
143
770
|
var loadingScript;
|
|
@@ -283,6 +910,21 @@ async function resumeCheckout(result, actions) {
|
|
|
283
910
|
actions.onProcessing?.(status);
|
|
284
911
|
return void 0;
|
|
285
912
|
}
|
|
913
|
+
|
|
914
|
+
// src/components/billing/checkout-navigation.ts
|
|
915
|
+
function navigateToCheckout(url, location = window.location) {
|
|
916
|
+
const parsed = new URL(
|
|
917
|
+
url,
|
|
918
|
+
typeof window !== "undefined" ? window.location.href : void 0
|
|
919
|
+
);
|
|
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);
|
|
927
|
+
}
|
|
286
928
|
async function handleCheckoutResult(result, options) {
|
|
287
929
|
if ("type" in result && result.type === "scheduled") {
|
|
288
930
|
options.onScheduled?.(result);
|
|
@@ -302,11 +944,11 @@ async function handleCheckoutResult(result, options) {
|
|
|
302
944
|
return;
|
|
303
945
|
}
|
|
304
946
|
if ("type" in result && result.type === "hosted") {
|
|
305
|
-
|
|
947
|
+
navigateToCheckout(result.payment_url);
|
|
306
948
|
return;
|
|
307
949
|
}
|
|
308
950
|
if ("url" in result && result.url) {
|
|
309
|
-
|
|
951
|
+
navigateToCheckout(result.url);
|
|
310
952
|
return;
|
|
311
953
|
}
|
|
312
954
|
if ("type" in result && result.type === "stripe") {
|
|
@@ -379,9 +1021,9 @@ function CheckoutButton({
|
|
|
379
1021
|
setProcessing(false);
|
|
380
1022
|
onSuccess?.(id);
|
|
381
1023
|
},
|
|
382
|
-
onComplete: (
|
|
1024
|
+
onComplete: (completed2) => {
|
|
383
1025
|
setProcessing(false);
|
|
384
|
-
onComplete?.(
|
|
1026
|
+
onComplete?.(completed2);
|
|
385
1027
|
},
|
|
386
1028
|
onScheduled
|
|
387
1029
|
});
|
|
@@ -421,962 +1063,2474 @@ function CheckoutButton({
|
|
|
421
1063
|
error && /* @__PURE__ */ jsx("span", { role: "alert", children: error.message })
|
|
422
1064
|
] });
|
|
423
1065
|
}
|
|
424
|
-
var
|
|
425
|
-
function
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return /enterprise|custom/i.test(p.name + p.code);
|
|
432
|
-
}
|
|
433
|
-
function v(name, fb) {
|
|
434
|
-
return `var(${name}, ${fb})`;
|
|
435
|
-
}
|
|
436
|
-
var C = {
|
|
437
|
-
bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
|
|
438
|
-
cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
|
|
439
|
-
hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
|
|
440
|
-
border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
|
|
441
|
-
radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
|
|
442
|
-
text: v("--nozle-foreground", "#111827"),
|
|
443
|
-
muted: v("--nozle-muted-foreground", "#6b7280"),
|
|
444
|
-
mutedBg: v("--nozle-muted", "#f3f4f6"),
|
|
445
|
-
hlFg: v("--nozle-primary-foreground", "#ffffff")
|
|
446
|
-
};
|
|
447
|
-
function Check() {
|
|
448
|
-
return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
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;
|
|
449
1073
|
}
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
1090
|
};
|
|
458
|
-
const tab = (active) => ({
|
|
459
|
-
padding: "8px 20px",
|
|
460
|
-
borderRadius: "9999px",
|
|
461
|
-
border: "none",
|
|
462
|
-
background: active ? C.cardBg : "transparent",
|
|
463
|
-
color: active ? C.text : C.muted,
|
|
464
|
-
fontWeight: active ? 600 : 400,
|
|
465
|
-
fontSize: "0.875rem",
|
|
466
|
-
cursor: "pointer",
|
|
467
|
-
boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
|
|
468
|
-
transition: "all 0.15s ease"
|
|
469
|
-
});
|
|
470
|
-
return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxs("div", { style: pill, children: [
|
|
471
|
-
/* @__PURE__ */ jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
|
|
472
|
-
/* @__PURE__ */ jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
|
|
473
|
-
"Annual",
|
|
474
|
-
/* @__PURE__ */ jsx("span", { style: {
|
|
475
|
-
marginLeft: "6px",
|
|
476
|
-
fontSize: "0.7rem",
|
|
477
|
-
fontWeight: 600,
|
|
478
|
-
color: isAnnual ? C.hl : C.muted
|
|
479
|
-
}, children: "Save 20%" })
|
|
480
|
-
] })
|
|
481
|
-
] }) });
|
|
482
1091
|
}
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1092
|
+
function CheckoutInner({
|
|
1093
|
+
submitLabel,
|
|
1094
|
+
onSuccess,
|
|
1095
|
+
onError,
|
|
1096
|
+
onReady,
|
|
1097
|
+
className,
|
|
1098
|
+
style,
|
|
1099
|
+
returnUrl,
|
|
1100
|
+
children
|
|
491
1101
|
}) {
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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);
|
|
500
1135
|
}
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
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,
|
|
505
1231
|
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
borderTopWidth: isHighlighted ? "3px" : "1px",
|
|
515
|
-
borderRadius: C.radius,
|
|
516
|
-
padding: "2rem",
|
|
517
|
-
transition: "box-shadow 0.2s ease, transform 0.2s ease",
|
|
518
|
-
boxShadow: shadow,
|
|
519
|
-
transform: hover && !isCurrent ? "translateY(-2px)" : "none"
|
|
520
|
-
},
|
|
521
|
-
children: [
|
|
522
|
-
isHighlighted && /* @__PURE__ */ jsx("div", { style: {
|
|
523
|
-
position: "absolute",
|
|
524
|
-
top: "-1px",
|
|
525
|
-
left: "50%",
|
|
526
|
-
transform: "translate(-50%, -50%)",
|
|
527
|
-
background: C.hl,
|
|
528
|
-
color: C.hlFg,
|
|
529
|
-
fontSize: "0.7rem",
|
|
530
|
-
fontWeight: 700,
|
|
531
|
-
padding: "4px 12px",
|
|
532
|
-
borderRadius: "9999px",
|
|
533
|
-
letterSpacing: "0.025em",
|
|
534
|
-
textTransform: "uppercase",
|
|
535
|
-
whiteSpace: "nowrap"
|
|
536
|
-
}, children: isCurrent ? "Current Plan" : "Most Popular" }),
|
|
537
|
-
isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { style: {
|
|
538
|
-
position: "absolute",
|
|
539
|
-
top: "0",
|
|
540
|
-
left: "50%",
|
|
541
|
-
transform: "translate(-50%, -50%)",
|
|
542
|
-
background: C.mutedBg,
|
|
543
|
-
color: C.muted,
|
|
544
|
-
fontSize: "0.7rem",
|
|
545
|
-
fontWeight: 700,
|
|
546
|
-
padding: "4px 12px",
|
|
547
|
-
borderRadius: "9999px",
|
|
548
|
-
letterSpacing: "0.025em",
|
|
549
|
-
textTransform: "uppercase",
|
|
550
|
-
whiteSpace: "nowrap"
|
|
551
|
-
}, children: "Current Plan" }),
|
|
552
|
-
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
|
|
553
|
-
/* @__PURE__ */ jsx("h3", { style: {
|
|
554
|
-
fontSize: "1.25rem",
|
|
555
|
-
fontWeight: 700,
|
|
556
|
-
color: C.text,
|
|
557
|
-
margin: 0,
|
|
558
|
-
lineHeight: 1.3
|
|
559
|
-
}, children: plan.name }),
|
|
560
|
-
plan.description && /* @__PURE__ */ jsx("p", { style: {
|
|
561
|
-
fontSize: "0.875rem",
|
|
562
|
-
color: C.muted,
|
|
563
|
-
margin: "6px 0 0",
|
|
564
|
-
lineHeight: 1.5
|
|
565
|
-
}, children: plan.description })
|
|
566
|
-
] }),
|
|
567
|
-
/* @__PURE__ */ jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
|
|
568
|
-
/* @__PURE__ */ jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
|
|
569
|
-
/* @__PURE__ */ jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
|
|
570
|
-
] }) }),
|
|
571
|
-
features && features.length > 0 && /* @__PURE__ */ jsx("ul", { style: {
|
|
572
|
-
listStyle: "none",
|
|
573
|
-
padding: 0,
|
|
574
|
-
margin: "0 0 2rem",
|
|
575
|
-
display: "flex",
|
|
576
|
-
flexDirection: "column",
|
|
577
|
-
gap: "0.75rem",
|
|
578
|
-
flex: 1
|
|
579
|
-
}, children: features.map((feat) => /* @__PURE__ */ jsxs("li", { style: {
|
|
580
|
-
display: "flex",
|
|
581
|
-
alignItems: "flex-start",
|
|
582
|
-
gap: "10px",
|
|
583
|
-
fontSize: "0.875rem",
|
|
584
|
-
color: C.text,
|
|
585
|
-
lineHeight: 1.5
|
|
586
|
-
}, children: [
|
|
587
|
-
/* @__PURE__ */ jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
|
|
588
|
-
/* @__PURE__ */ jsx("span", { children: feat })
|
|
589
|
-
] }, feat)) }),
|
|
590
|
-
(!features || features.length === 0) && /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
|
|
591
|
-
onSelect && /* @__PURE__ */ jsx(
|
|
592
|
-
CTAButton,
|
|
593
|
-
{
|
|
594
|
-
onClick: handleClick,
|
|
595
|
-
disabled: isCurrent,
|
|
596
|
-
variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
|
|
597
|
-
children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
|
|
598
|
-
}
|
|
599
|
-
)
|
|
600
|
-
]
|
|
1232
|
+
submitLabel,
|
|
1233
|
+
onSuccess,
|
|
1234
|
+
onError,
|
|
1235
|
+
onReady,
|
|
1236
|
+
className,
|
|
1237
|
+
style,
|
|
1238
|
+
returnUrl,
|
|
1239
|
+
children
|
|
601
1240
|
}
|
|
602
|
-
);
|
|
1241
|
+
) }, clientSecret);
|
|
603
1242
|
}
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
color: C.muted
|
|
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);
|
|
640
1278
|
}
|
|
641
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
|
+
}
|
|
642
1300
|
return /* @__PURE__ */ jsx(
|
|
643
|
-
|
|
1301
|
+
CheckoutContext.Provider,
|
|
644
1302
|
{
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
] })
|
|
651
1318
|
}
|
|
652
1319
|
);
|
|
653
1320
|
}
|
|
654
|
-
function
|
|
655
|
-
return /* @__PURE__ */ jsx(
|
|
656
|
-
display: "grid",
|
|
657
|
-
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
658
|
-
gap: "1.5rem"
|
|
659
|
-
}, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { style: {
|
|
660
|
-
background: C.cardBg,
|
|
661
|
-
border: `1px solid ${C.border}`,
|
|
662
|
-
borderRadius: C.radius,
|
|
663
|
-
padding: "2rem",
|
|
664
|
-
height: "380px"
|
|
665
|
-
}, children: [
|
|
666
|
-
/* @__PURE__ */ jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
|
|
667
|
-
/* @__PURE__ */ jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
|
|
668
|
-
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
669
|
-
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
670
|
-
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
|
|
671
|
-
] }, i)) });
|
|
1321
|
+
function Checkout(props) {
|
|
1322
|
+
return "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props });
|
|
672
1323
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
enterpriseEmail
|
|
686
|
-
}) {
|
|
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) {
|
|
687
1336
|
const billing = useOptionalBillingContext();
|
|
688
|
-
const client = billing?.client;
|
|
689
|
-
const [plans, setPlans] = useState(propPlans ?? []);
|
|
690
|
-
const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
|
|
691
|
-
const [isAnnual, setIsAnnual] = useState(false);
|
|
692
|
-
const [loading, setLoading] = useState(!propPlans);
|
|
693
1337
|
const [processing, setProcessing] = useState(false);
|
|
694
|
-
const [
|
|
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);
|
|
695
1344
|
useEffect(() => {
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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);
|
|
704
1400
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
]
|
|
710
1484
|
}
|
|
711
|
-
|
|
712
|
-
|
|
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);
|
|
713
1692
|
});
|
|
714
1693
|
return () => {
|
|
715
|
-
|
|
1694
|
+
inFlight.current?.abort();
|
|
1695
|
+
owner.controller.abort();
|
|
716
1696
|
};
|
|
717
|
-
}, [
|
|
1697
|
+
}, [owner]);
|
|
718
1698
|
useEffect(() => {
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (!billing?.createCheckout) {
|
|
729
|
-
throw new Error("BillingProvider createCheckout callback is required for checkout");
|
|
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;
|
|
730
1708
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
returnUrl: returnUrl ?? window.location.href
|
|
734
|
-
});
|
|
735
|
-
onCheckoutResult?.(result);
|
|
736
|
-
await handleCheckoutResult(result, {
|
|
737
|
-
onStripeClientSecret,
|
|
738
|
-
verifyCheckout: billing?.verifyCheckout,
|
|
739
|
-
getCheckoutStatus: billing?.getCheckoutStatus,
|
|
740
|
-
onProcessing: () => setProcessing(true),
|
|
741
|
-
onSuccess: () => setProcessing(false),
|
|
742
|
-
onComplete: () => setProcessing(false)
|
|
1709
|
+
void run(async (signal) => {
|
|
1710
|
+
await reconcile(signal);
|
|
743
1711
|
});
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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;
|
|
748
1736
|
}
|
|
1737
|
+
setTarget("");
|
|
1738
|
+
setQuote(void 0);
|
|
1739
|
+
setMode("choose");
|
|
1740
|
+
setFailure(void 0);
|
|
1741
|
+
void run(async (signal) => {
|
|
1742
|
+
await read(signal);
|
|
1743
|
+
});
|
|
749
1744
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
] })
|
|
778
2057
|
] });
|
|
779
2058
|
}
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
791
2071
|
}) {
|
|
792
|
-
const
|
|
793
|
-
const
|
|
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);
|
|
794
2108
|
return /* @__PURE__ */ jsxs(
|
|
795
2109
|
"div",
|
|
796
2110
|
{
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
background: "var(--nozle-background, var(--background))",
|
|
801
|
-
border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
|
|
802
|
-
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
803
|
-
padding: "1.5rem",
|
|
804
|
-
display: "flex",
|
|
805
|
-
flexDirection: "column",
|
|
806
|
-
gap: "1rem"
|
|
807
|
-
},
|
|
808
|
-
className: isCurrent ? "border-primary" : "",
|
|
2111
|
+
className: `nozle-billing-portal ${className}`,
|
|
2112
|
+
style,
|
|
2113
|
+
"aria-label": "Nozle billing portal",
|
|
809
2114
|
children: [
|
|
810
|
-
/* @__PURE__ */
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
{
|
|
814
|
-
style: {
|
|
815
|
-
fontSize: "1.25rem",
|
|
816
|
-
fontWeight: 600,
|
|
817
|
-
color: "var(--nozle-foreground, var(--foreground))",
|
|
818
|
-
margin: 0
|
|
819
|
-
},
|
|
820
|
-
children: name
|
|
821
|
-
}
|
|
822
|
-
),
|
|
823
|
-
description && /* @__PURE__ */ jsx(
|
|
824
|
-
"p",
|
|
825
|
-
{
|
|
826
|
-
style: {
|
|
827
|
-
color: "var(--nozle-muted-foreground, var(--muted-foreground))",
|
|
828
|
-
fontSize: "0.875rem",
|
|
829
|
-
margin: "0.25rem 0 0"
|
|
830
|
-
},
|
|
831
|
-
children: description
|
|
832
|
-
}
|
|
833
|
-
)
|
|
834
|
-
] }),
|
|
835
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
836
|
-
/* @__PURE__ */ jsxs(
|
|
837
|
-
"span",
|
|
838
|
-
{
|
|
839
|
-
style: {
|
|
840
|
-
fontSize: "2rem",
|
|
841
|
-
fontWeight: 700,
|
|
842
|
-
color: "var(--nozle-foreground, var(--foreground))"
|
|
843
|
-
},
|
|
844
|
-
children: [
|
|
845
|
-
"$",
|
|
846
|
-
price
|
|
847
|
-
]
|
|
848
|
-
}
|
|
849
|
-
),
|
|
850
|
-
/* @__PURE__ */ jsxs(
|
|
851
|
-
"span",
|
|
852
|
-
{
|
|
853
|
-
style: {
|
|
854
|
-
color: "var(--nozle-muted-foreground, var(--muted-foreground))",
|
|
855
|
-
marginLeft: "0.25rem"
|
|
856
|
-
},
|
|
857
|
-
children: [
|
|
858
|
-
"/",
|
|
859
|
-
interval
|
|
860
|
-
]
|
|
861
|
-
}
|
|
862
|
-
)
|
|
863
|
-
] }),
|
|
864
|
-
/* @__PURE__ */ jsx(
|
|
865
|
-
"ul",
|
|
866
|
-
{
|
|
867
|
-
style: {
|
|
868
|
-
listStyle: "none",
|
|
869
|
-
padding: 0,
|
|
870
|
-
margin: 0,
|
|
871
|
-
display: "flex",
|
|
872
|
-
flexDirection: "column",
|
|
873
|
-
gap: "0.5rem",
|
|
874
|
-
flex: 1
|
|
875
|
-
},
|
|
876
|
-
children: features.map((feature) => /* @__PURE__ */ jsxs(
|
|
877
|
-
"li",
|
|
878
|
-
{
|
|
879
|
-
style: {
|
|
880
|
-
display: "flex",
|
|
881
|
-
alignItems: "center",
|
|
882
|
-
gap: "0.5rem",
|
|
883
|
-
color: "var(--nozle-foreground, var(--foreground))",
|
|
884
|
-
fontSize: "0.875rem"
|
|
885
|
-
},
|
|
886
|
-
children: [
|
|
887
|
-
/* @__PURE__ */ jsx(
|
|
888
|
-
"span",
|
|
889
|
-
{
|
|
890
|
-
style: { color: "var(--nozle-primary, var(--primary))" },
|
|
891
|
-
"aria-hidden": "true",
|
|
892
|
-
children: "\u2713"
|
|
893
|
-
}
|
|
894
|
-
),
|
|
895
|
-
feature
|
|
896
|
-
]
|
|
897
|
-
},
|
|
898
|
-
feature
|
|
899
|
-
))
|
|
900
|
-
}
|
|
901
|
-
),
|
|
902
|
-
onSelect && /* @__PURE__ */ jsx(
|
|
903
|
-
"button",
|
|
2115
|
+
/* @__PURE__ */ jsx("style", { nonce, children: portalStyles }),
|
|
2116
|
+
current?.client ? /* @__PURE__ */ jsx(
|
|
2117
|
+
Portal,
|
|
904
2118
|
{
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
cursor: isCurrent ? "default" : "pointer",
|
|
916
|
-
fontWeight: 500,
|
|
917
|
-
width: "100%"
|
|
918
|
-
},
|
|
919
|
-
disabled: isCurrent,
|
|
920
|
-
"aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
|
|
921
|
-
children: isCurrent ? "Current Plan" : "Get Started"
|
|
922
|
-
}
|
|
923
|
-
)
|
|
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" })
|
|
924
2129
|
]
|
|
925
2130
|
}
|
|
926
2131
|
);
|
|
927
2132
|
}
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
931
2196
|
}) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
{
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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",
|
|
941
2398
|
{
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
borderCollapse: "collapse",
|
|
945
|
-
fontSize: "0.875rem",
|
|
946
|
-
color: "var(--nozle-foreground, var(--foreground))"
|
|
947
|
-
},
|
|
2399
|
+
className: "nzp-card",
|
|
2400
|
+
"data-subscription-id": subscription.externalId,
|
|
948
2401
|
children: [
|
|
949
|
-
/* @__PURE__ */
|
|
950
|
-
/* @__PURE__ */ jsx(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
fontWeight: 500
|
|
959
|
-
},
|
|
960
|
-
children: "Feature"
|
|
961
|
-
}
|
|
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
|
|
962
2411
|
),
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
+
] }),
|
|
979
2431
|
/* @__PURE__ */ jsx(
|
|
980
|
-
"
|
|
2432
|
+
"button",
|
|
981
2433
|
{
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
color: "var(--nozle-foreground, var(--foreground))"
|
|
986
|
-
},
|
|
987
|
-
children: feature.label
|
|
2434
|
+
type: "button",
|
|
2435
|
+
onClick: () => onUsage(subscription),
|
|
2436
|
+
children: "View usage"
|
|
988
2437
|
}
|
|
989
|
-
)
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
)
|
|
1024
2492
|
]
|
|
2493
|
+
},
|
|
2494
|
+
subscription.externalId || subscription.id
|
|
2495
|
+
)) }),
|
|
2496
|
+
/* @__PURE__ */ jsx(
|
|
2497
|
+
Pager,
|
|
2498
|
+
{
|
|
2499
|
+
page,
|
|
2500
|
+
total: result.metadata.totalPages,
|
|
2501
|
+
setPage
|
|
1025
2502
|
}
|
|
1026
2503
|
)
|
|
1027
|
-
}
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
var CheckoutContext = createContext(null);
|
|
1031
|
-
function useCheckout2() {
|
|
1032
|
-
const ctx = useContext(CheckoutContext);
|
|
1033
|
-
if (ctx === null) {
|
|
1034
|
-
throw new Error("useCheckout must be used inside a <Checkout> component");
|
|
1035
|
-
}
|
|
1036
|
-
return ctx;
|
|
2504
|
+
] })
|
|
2505
|
+
] });
|
|
1037
2506
|
}
|
|
1038
|
-
function
|
|
1039
|
-
if (typeof document === "undefined") {
|
|
1040
|
-
return { theme: "stripe" };
|
|
1041
|
-
}
|
|
1042
|
-
const style = getComputedStyle(document.documentElement);
|
|
1043
|
-
const get = (prop, fallback) => style.getPropertyValue(prop).trim() || fallback;
|
|
2507
|
+
function cancellationState(subscription) {
|
|
1044
2508
|
return {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
borderRadius: get("--nozle-radius", "0.5rem"),
|
|
1052
|
-
fontFamily: "inherit"
|
|
1053
|
-
}
|
|
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
|
|
1054
2515
|
};
|
|
1055
2516
|
}
|
|
1056
|
-
function
|
|
1057
|
-
|
|
1058
|
-
|
|
2517
|
+
function UsageDetails({
|
|
2518
|
+
client,
|
|
2519
|
+
locale,
|
|
2520
|
+
reconnect,
|
|
1059
2521
|
onError,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
style,
|
|
1063
|
-
returnUrl,
|
|
1064
|
-
children
|
|
2522
|
+
subscription,
|
|
2523
|
+
timezone
|
|
1065
2524
|
}) {
|
|
1066
|
-
const
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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();
|
|
1079
2702
|
});
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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",
|
|
1093
2807
|
{
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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");
|
|
1101
2900
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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;
|
|
1110
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);
|
|
1111
2928
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
2929
|
+
},
|
|
2930
|
+
[createCheckout]
|
|
2931
|
+
);
|
|
2932
|
+
return { fetchClientSecret, checkout, isLoading, error };
|
|
2933
|
+
}
|
|
2934
|
+
var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
|
|
2935
|
+
function fmtPrice(cents, cur) {
|
|
2936
|
+
const s = SYMBOLS[cur] ?? cur + " ";
|
|
2937
|
+
const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
|
|
2938
|
+
return `${s}${val}`;
|
|
2939
|
+
}
|
|
2940
|
+
function isEnterprise(p) {
|
|
2941
|
+
return /enterprise|custom/i.test(p.name + p.code);
|
|
2942
|
+
}
|
|
2943
|
+
function v(name, fb) {
|
|
2944
|
+
return `var(${name}, ${fb})`;
|
|
2945
|
+
}
|
|
2946
|
+
var C = {
|
|
2947
|
+
bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
|
|
2948
|
+
cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
|
|
2949
|
+
hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
|
|
2950
|
+
border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
|
|
2951
|
+
radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
|
|
2952
|
+
text: v("--nozle-foreground", "#111827"),
|
|
2953
|
+
muted: v("--nozle-muted-foreground", "#6b7280"),
|
|
2954
|
+
mutedBg: v("--nozle-muted", "#f3f4f6"),
|
|
2955
|
+
hlFg: v("--nozle-primary-foreground", "#ffffff")
|
|
2956
|
+
};
|
|
2957
|
+
function Check() {
|
|
2958
|
+
return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
2959
|
+
}
|
|
2960
|
+
function Toggle({ isAnnual, onChange }) {
|
|
2961
|
+
const pill = {
|
|
2962
|
+
display: "inline-flex",
|
|
2963
|
+
borderRadius: "9999px",
|
|
2964
|
+
padding: "4px",
|
|
2965
|
+
background: C.mutedBg,
|
|
2966
|
+
gap: "2px"
|
|
2967
|
+
};
|
|
2968
|
+
const tab = (active) => ({
|
|
2969
|
+
padding: "8px 20px",
|
|
2970
|
+
borderRadius: "9999px",
|
|
2971
|
+
border: "none",
|
|
2972
|
+
background: active ? C.cardBg : "transparent",
|
|
2973
|
+
color: active ? C.text : C.muted,
|
|
2974
|
+
fontWeight: active ? 600 : 400,
|
|
2975
|
+
fontSize: "0.875rem",
|
|
2976
|
+
cursor: "pointer",
|
|
2977
|
+
boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
|
|
2978
|
+
transition: "all 0.15s ease"
|
|
2979
|
+
});
|
|
2980
|
+
return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxs("div", { style: pill, children: [
|
|
2981
|
+
/* @__PURE__ */ jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
|
|
2982
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
|
|
2983
|
+
"Annual",
|
|
2984
|
+
/* @__PURE__ */ jsx("span", { style: {
|
|
2985
|
+
marginLeft: "6px",
|
|
2986
|
+
fontSize: "0.7rem",
|
|
2987
|
+
fontWeight: 600,
|
|
2988
|
+
color: isAnnual ? C.hl : C.muted
|
|
2989
|
+
}, children: "Save 20%" })
|
|
2990
|
+
] })
|
|
1145
2991
|
] }) });
|
|
1146
2992
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
onError,
|
|
1156
|
-
onReady,
|
|
1157
|
-
className,
|
|
1158
|
-
style,
|
|
1159
|
-
children
|
|
2993
|
+
function Card({
|
|
2994
|
+
plan,
|
|
2995
|
+
features,
|
|
2996
|
+
isCurrent,
|
|
2997
|
+
isHighlighted,
|
|
2998
|
+
isEnt,
|
|
2999
|
+
enterpriseEmail,
|
|
3000
|
+
onSelect
|
|
1160
3001
|
}) {
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
)
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
3002
|
+
const [hover, setHover] = useState(false);
|
|
3003
|
+
const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
|
|
3004
|
+
const interval = plan.interval === "yearly" ? "/year" : "/month";
|
|
3005
|
+
const handleClick = () => {
|
|
3006
|
+
if (isCurrent) return;
|
|
3007
|
+
if (isEnt && enterpriseEmail) {
|
|
3008
|
+
window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
onSelect?.();
|
|
3012
|
+
};
|
|
3013
|
+
return /* @__PURE__ */ jsxs(
|
|
3014
|
+
"div",
|
|
3015
|
+
{
|
|
3016
|
+
onMouseEnter: () => setHover(true),
|
|
3017
|
+
onMouseLeave: () => setHover(false),
|
|
3018
|
+
style: {
|
|
3019
|
+
position: "relative",
|
|
3020
|
+
display: "flex",
|
|
3021
|
+
flexDirection: "column",
|
|
3022
|
+
background: C.cardBg,
|
|
3023
|
+
border: `1px solid ${isHighlighted ? C.hl : C.border}`,
|
|
3024
|
+
borderTopWidth: isHighlighted ? "3px" : "1px",
|
|
3025
|
+
borderRadius: C.radius,
|
|
3026
|
+
padding: "2rem",
|
|
3027
|
+
transition: "box-shadow 0.2s ease, transform 0.2s ease",
|
|
3028
|
+
boxShadow: shadow,
|
|
3029
|
+
transform: hover && !isCurrent ? "translateY(-2px)" : "none"
|
|
3030
|
+
},
|
|
3031
|
+
children: [
|
|
3032
|
+
isHighlighted && /* @__PURE__ */ jsx("div", { style: {
|
|
3033
|
+
position: "absolute",
|
|
3034
|
+
top: "-1px",
|
|
3035
|
+
left: "50%",
|
|
3036
|
+
transform: "translate(-50%, -50%)",
|
|
3037
|
+
background: C.hl,
|
|
3038
|
+
color: C.hlFg,
|
|
3039
|
+
fontSize: "0.7rem",
|
|
3040
|
+
fontWeight: 700,
|
|
3041
|
+
padding: "4px 12px",
|
|
3042
|
+
borderRadius: "9999px",
|
|
3043
|
+
letterSpacing: "0.025em",
|
|
3044
|
+
textTransform: "uppercase",
|
|
3045
|
+
whiteSpace: "nowrap"
|
|
3046
|
+
}, children: isCurrent ? "Current Plan" : "Most Popular" }),
|
|
3047
|
+
isCurrent && !isHighlighted && /* @__PURE__ */ jsx("div", { style: {
|
|
3048
|
+
position: "absolute",
|
|
3049
|
+
top: "0",
|
|
3050
|
+
left: "50%",
|
|
3051
|
+
transform: "translate(-50%, -50%)",
|
|
3052
|
+
background: C.mutedBg,
|
|
3053
|
+
color: C.muted,
|
|
3054
|
+
fontSize: "0.7rem",
|
|
3055
|
+
fontWeight: 700,
|
|
3056
|
+
padding: "4px 12px",
|
|
3057
|
+
borderRadius: "9999px",
|
|
3058
|
+
letterSpacing: "0.025em",
|
|
3059
|
+
textTransform: "uppercase",
|
|
3060
|
+
whiteSpace: "nowrap"
|
|
3061
|
+
}, children: "Current Plan" }),
|
|
3062
|
+
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
|
|
3063
|
+
/* @__PURE__ */ jsx("h3", { style: {
|
|
3064
|
+
fontSize: "1.25rem",
|
|
3065
|
+
fontWeight: 700,
|
|
3066
|
+
color: C.text,
|
|
3067
|
+
margin: 0,
|
|
3068
|
+
lineHeight: 1.3
|
|
3069
|
+
}, children: plan.name }),
|
|
3070
|
+
plan.description && /* @__PURE__ */ jsx("p", { style: {
|
|
3071
|
+
fontSize: "0.875rem",
|
|
3072
|
+
color: C.muted,
|
|
3073
|
+
margin: "6px 0 0",
|
|
3074
|
+
lineHeight: 1.5
|
|
3075
|
+
}, children: plan.description })
|
|
3076
|
+
] }),
|
|
3077
|
+
/* @__PURE__ */ jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
|
|
3078
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
|
|
3079
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
|
|
3080
|
+
] }) }),
|
|
3081
|
+
features && features.length > 0 && /* @__PURE__ */ jsx("ul", { style: {
|
|
3082
|
+
listStyle: "none",
|
|
3083
|
+
padding: 0,
|
|
3084
|
+
margin: "0 0 2rem",
|
|
3085
|
+
display: "flex",
|
|
3086
|
+
flexDirection: "column",
|
|
3087
|
+
gap: "0.75rem",
|
|
3088
|
+
flex: 1
|
|
3089
|
+
}, children: features.map((feat) => /* @__PURE__ */ jsxs("li", { style: {
|
|
3090
|
+
display: "flex",
|
|
3091
|
+
alignItems: "flex-start",
|
|
3092
|
+
gap: "10px",
|
|
3093
|
+
fontSize: "0.875rem",
|
|
3094
|
+
color: C.text,
|
|
3095
|
+
lineHeight: 1.5
|
|
3096
|
+
}, children: [
|
|
3097
|
+
/* @__PURE__ */ jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
|
|
3098
|
+
/* @__PURE__ */ jsx("span", { children: feat })
|
|
3099
|
+
] }, feat)) }),
|
|
3100
|
+
(!features || features.length === 0) && /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
|
|
3101
|
+
onSelect && /* @__PURE__ */ jsx(
|
|
3102
|
+
CTAButton,
|
|
3103
|
+
{
|
|
3104
|
+
onClick: handleClick,
|
|
3105
|
+
disabled: isCurrent,
|
|
3106
|
+
variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
|
|
3107
|
+
children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
|
|
3108
|
+
}
|
|
3109
|
+
)
|
|
3110
|
+
]
|
|
3111
|
+
}
|
|
1169
3112
|
);
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
3113
|
+
}
|
|
3114
|
+
function CTAButton({
|
|
3115
|
+
children,
|
|
3116
|
+
onClick,
|
|
3117
|
+
disabled,
|
|
3118
|
+
variant
|
|
3119
|
+
}) {
|
|
3120
|
+
const [hover, setHover] = useState(false);
|
|
3121
|
+
const base = {
|
|
3122
|
+
width: "100%",
|
|
3123
|
+
padding: "12px 24px",
|
|
3124
|
+
borderRadius: "10px",
|
|
3125
|
+
fontSize: "0.9375rem",
|
|
3126
|
+
fontWeight: 600,
|
|
3127
|
+
cursor: disabled ? "default" : "pointer",
|
|
3128
|
+
border: "none",
|
|
3129
|
+
transition: "all 0.15s ease",
|
|
3130
|
+
lineHeight: 1.4
|
|
3131
|
+
};
|
|
3132
|
+
const variants = {
|
|
3133
|
+
primary: {
|
|
3134
|
+
background: hover && !disabled ? "#4f46e5" : C.hl,
|
|
3135
|
+
color: C.hlFg
|
|
3136
|
+
},
|
|
3137
|
+
secondary: {
|
|
3138
|
+
background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
|
|
3139
|
+
color: C.text,
|
|
3140
|
+
border: `1px solid ${C.border}`
|
|
3141
|
+
},
|
|
3142
|
+
outline: {
|
|
3143
|
+
background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
|
|
3144
|
+
color: C.hl,
|
|
3145
|
+
border: `1.5px solid ${C.hl}`
|
|
3146
|
+
},
|
|
3147
|
+
current: {
|
|
3148
|
+
background: C.mutedBg,
|
|
3149
|
+
color: C.muted
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
return /* @__PURE__ */ jsx(
|
|
3153
|
+
"button",
|
|
1183
3154
|
{
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
style,
|
|
1190
|
-
returnUrl,
|
|
3155
|
+
onClick: disabled ? void 0 : onClick,
|
|
3156
|
+
disabled,
|
|
3157
|
+
onMouseEnter: () => setHover(true),
|
|
3158
|
+
onMouseLeave: () => setHover(false),
|
|
3159
|
+
style: { ...base, ...variants[variant] },
|
|
1191
3160
|
children
|
|
1192
3161
|
}
|
|
1193
|
-
)
|
|
3162
|
+
);
|
|
1194
3163
|
}
|
|
1195
|
-
function
|
|
3164
|
+
function Skeleton() {
|
|
3165
|
+
return /* @__PURE__ */ jsx("div", { style: {
|
|
3166
|
+
display: "grid",
|
|
3167
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
3168
|
+
gap: "1.5rem"
|
|
3169
|
+
}, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { style: {
|
|
3170
|
+
background: C.cardBg,
|
|
3171
|
+
border: `1px solid ${C.border}`,
|
|
3172
|
+
borderRadius: C.radius,
|
|
3173
|
+
padding: "2rem",
|
|
3174
|
+
height: "380px"
|
|
3175
|
+
}, children: [
|
|
3176
|
+
/* @__PURE__ */ jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
|
|
3177
|
+
/* @__PURE__ */ jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
|
|
3178
|
+
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
3179
|
+
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
|
|
3180
|
+
/* @__PURE__ */ jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
|
|
3181
|
+
] }, i)) });
|
|
3182
|
+
}
|
|
3183
|
+
function PricingTable({
|
|
3184
|
+
currentPlanCode,
|
|
3185
|
+
plans: propPlans,
|
|
3186
|
+
features,
|
|
3187
|
+
onSelect,
|
|
3188
|
+
returnUrl,
|
|
3189
|
+
onStripeClientSecret,
|
|
3190
|
+
onCheckoutResult,
|
|
3191
|
+
onCheckoutError,
|
|
3192
|
+
showToggle = true,
|
|
3193
|
+
highlightPlan,
|
|
3194
|
+
className,
|
|
3195
|
+
enterpriseEmail
|
|
3196
|
+
}) {
|
|
1196
3197
|
const billing = useOptionalBillingContext();
|
|
1197
|
-
const
|
|
1198
|
-
const [
|
|
3198
|
+
const client = billing?.client;
|
|
3199
|
+
const [plans, setPlans] = useState(propPlans ?? []);
|
|
3200
|
+
const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
|
|
3201
|
+
const [isAnnual, setIsAnnual] = useState(false);
|
|
3202
|
+
const [loading, setLoading] = useState(!propPlans);
|
|
1199
3203
|
const [processing, setProcessing] = useState(false);
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
3204
|
+
const [checkoutError, setCheckoutError] = useState(null);
|
|
3205
|
+
useEffect(() => {
|
|
3206
|
+
if (propPlans) {
|
|
3207
|
+
setPlans(propPlans);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
if (!client) {
|
|
3211
|
+
setLoading(false);
|
|
3212
|
+
setCheckoutError("BillingProvider with a publishableKey is required to load plans");
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
let cancelled = false;
|
|
3216
|
+
client.catalogFetch("/api/v1/plans").then(async (res) => {
|
|
3217
|
+
if (!cancelled && res.ok) {
|
|
3218
|
+
const data = await res.json();
|
|
3219
|
+
setPlans(data.plans || []);
|
|
3220
|
+
}
|
|
3221
|
+
}).finally(() => {
|
|
3222
|
+
if (!cancelled) setLoading(false);
|
|
3223
|
+
});
|
|
3224
|
+
return () => {
|
|
3225
|
+
cancelled = true;
|
|
3226
|
+
};
|
|
3227
|
+
}, [client, propPlans]);
|
|
3228
|
+
useEffect(() => {
|
|
3229
|
+
if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
|
|
3230
|
+
}, [currentPlanCode]);
|
|
3231
|
+
async function selectPlan(plan) {
|
|
3232
|
+
setCheckoutError(null);
|
|
3233
|
+
if (onSelect) {
|
|
3234
|
+
onSelect(plan);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
1204
3237
|
try {
|
|
1205
|
-
|
|
3238
|
+
if (!billing?.createCheckout) {
|
|
3239
|
+
throw new Error("BillingProvider createCheckout callback is required for checkout");
|
|
3240
|
+
}
|
|
3241
|
+
const result = await billing.createCheckout({
|
|
3242
|
+
planCode: plan.code,
|
|
3243
|
+
returnUrl: returnUrl ?? window.location.href
|
|
3244
|
+
});
|
|
3245
|
+
onCheckoutResult?.(result);
|
|
3246
|
+
await handleCheckoutResult(result, {
|
|
3247
|
+
onStripeClientSecret,
|
|
1206
3248
|
verifyCheckout: billing?.verifyCheckout,
|
|
1207
3249
|
getCheckoutStatus: billing?.getCheckoutStatus,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
props.onComplete?.();
|
|
1212
|
-
},
|
|
1213
|
-
onComplete: () => {
|
|
1214
|
-
setProcessing(false);
|
|
1215
|
-
props.onComplete?.();
|
|
1216
|
-
},
|
|
1217
|
-
onProcessing: (status) => {
|
|
1218
|
-
setProcessing(true);
|
|
1219
|
-
props.onProcessing?.(status);
|
|
1220
|
-
}
|
|
3250
|
+
onProcessing: () => setProcessing(true),
|
|
3251
|
+
onSuccess: () => setProcessing(false),
|
|
3252
|
+
onComplete: () => setProcessing(false)
|
|
1221
3253
|
});
|
|
1222
3254
|
} catch (cause) {
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
} finally {
|
|
1227
|
-
setBusy(false);
|
|
3255
|
+
const error = cause instanceof Error ? cause : new Error("Checkout failed");
|
|
3256
|
+
setCheckoutError(error.message);
|
|
3257
|
+
onCheckoutError?.(error);
|
|
1228
3258
|
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
3259
|
+
}
|
|
3260
|
+
const hasAnnual = plans.some((p) => p.interval === "yearly");
|
|
3261
|
+
const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
|
|
3262
|
+
if (loading) return /* @__PURE__ */ jsx(Skeleton, {});
|
|
3263
|
+
return /* @__PURE__ */ jsxs("div", { className, style: {
|
|
3264
|
+
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
3265
|
+
color: C.text,
|
|
3266
|
+
background: C.bg
|
|
3267
|
+
}, children: [
|
|
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." }),
|
|
3270
|
+
checkoutError && /* @__PURE__ */ jsx("p", { role: "alert", children: checkoutError }),
|
|
3271
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
3272
|
+
display: "grid",
|
|
3273
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
3274
|
+
gap: "1.5rem"
|
|
3275
|
+
}, children: filtered.map((plan, i) => /* @__PURE__ */ jsx(
|
|
3276
|
+
Card,
|
|
3277
|
+
{
|
|
3278
|
+
plan,
|
|
3279
|
+
features: features?.[i],
|
|
3280
|
+
isCurrent: currentPlan === plan.code,
|
|
3281
|
+
isHighlighted: highlightPlan === plan.code,
|
|
3282
|
+
isEnt: isEnterprise(plan),
|
|
3283
|
+
enterpriseEmail,
|
|
3284
|
+
onSelect: () => void selectPlan(plan)
|
|
3285
|
+
},
|
|
3286
|
+
plan.code
|
|
3287
|
+
)) })
|
|
3288
|
+
] });
|
|
3289
|
+
}
|
|
3290
|
+
function PlanCard({
|
|
3291
|
+
id,
|
|
3292
|
+
name,
|
|
3293
|
+
monthlyPrice,
|
|
3294
|
+
annualPrice,
|
|
3295
|
+
features,
|
|
3296
|
+
description,
|
|
3297
|
+
isAnnual,
|
|
3298
|
+
isCurrent,
|
|
3299
|
+
returnUrl,
|
|
3300
|
+
onSelect
|
|
3301
|
+
}) {
|
|
3302
|
+
const price = isAnnual ? annualPrice : monthlyPrice;
|
|
3303
|
+
const interval = isAnnual ? "year" : "month";
|
|
3304
|
+
return /* @__PURE__ */ jsxs(
|
|
3305
|
+
"div",
|
|
1232
3306
|
{
|
|
1233
|
-
|
|
1234
|
-
|
|
3307
|
+
"data-testid": `plan-card-${id}`,
|
|
3308
|
+
"aria-current": isCurrent ? "true" : void 0,
|
|
3309
|
+
style: {
|
|
3310
|
+
background: "var(--nozle-background, var(--background))",
|
|
3311
|
+
border: isCurrent ? "2px solid var(--nozle-primary, var(--primary))" : "1px solid var(--nozle-border, var(--border))",
|
|
3312
|
+
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
3313
|
+
padding: "1.5rem",
|
|
3314
|
+
display: "flex",
|
|
3315
|
+
flexDirection: "column",
|
|
3316
|
+
gap: "1rem"
|
|
3317
|
+
},
|
|
3318
|
+
className: isCurrent ? "border-primary" : "",
|
|
3319
|
+
children: [
|
|
3320
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3321
|
+
/* @__PURE__ */ jsx(
|
|
3322
|
+
"h3",
|
|
3323
|
+
{
|
|
3324
|
+
style: {
|
|
3325
|
+
fontSize: "1.25rem",
|
|
3326
|
+
fontWeight: 600,
|
|
3327
|
+
color: "var(--nozle-foreground, var(--foreground))",
|
|
3328
|
+
margin: 0
|
|
3329
|
+
},
|
|
3330
|
+
children: name
|
|
3331
|
+
}
|
|
3332
|
+
),
|
|
3333
|
+
description && /* @__PURE__ */ jsx(
|
|
3334
|
+
"p",
|
|
3335
|
+
{
|
|
3336
|
+
style: {
|
|
3337
|
+
color: "var(--nozle-muted-foreground, var(--muted-foreground))",
|
|
3338
|
+
fontSize: "0.875rem",
|
|
3339
|
+
margin: "0.25rem 0 0"
|
|
3340
|
+
},
|
|
3341
|
+
children: description
|
|
3342
|
+
}
|
|
3343
|
+
)
|
|
3344
|
+
] }),
|
|
3345
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3346
|
+
/* @__PURE__ */ jsxs(
|
|
3347
|
+
"span",
|
|
3348
|
+
{
|
|
3349
|
+
style: {
|
|
3350
|
+
fontSize: "2rem",
|
|
3351
|
+
fontWeight: 700,
|
|
3352
|
+
color: "var(--nozle-foreground, var(--foreground))"
|
|
3353
|
+
},
|
|
3354
|
+
children: [
|
|
3355
|
+
"$",
|
|
3356
|
+
price
|
|
3357
|
+
]
|
|
3358
|
+
}
|
|
3359
|
+
),
|
|
3360
|
+
/* @__PURE__ */ jsxs(
|
|
3361
|
+
"span",
|
|
3362
|
+
{
|
|
3363
|
+
style: {
|
|
3364
|
+
color: "var(--nozle-muted-foreground, var(--muted-foreground))",
|
|
3365
|
+
marginLeft: "0.25rem"
|
|
3366
|
+
},
|
|
3367
|
+
children: [
|
|
3368
|
+
"/",
|
|
3369
|
+
interval
|
|
3370
|
+
]
|
|
3371
|
+
}
|
|
3372
|
+
)
|
|
3373
|
+
] }),
|
|
1235
3374
|
/* @__PURE__ */ jsx(
|
|
1236
|
-
"
|
|
3375
|
+
"ul",
|
|
1237
3376
|
{
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
3377
|
+
style: {
|
|
3378
|
+
listStyle: "none",
|
|
3379
|
+
padding: 0,
|
|
3380
|
+
margin: 0,
|
|
3381
|
+
display: "flex",
|
|
3382
|
+
flexDirection: "column",
|
|
3383
|
+
gap: "0.5rem",
|
|
3384
|
+
flex: 1
|
|
3385
|
+
},
|
|
3386
|
+
children: features.map((feature) => /* @__PURE__ */ jsxs(
|
|
3387
|
+
"li",
|
|
3388
|
+
{
|
|
3389
|
+
style: {
|
|
3390
|
+
display: "flex",
|
|
3391
|
+
alignItems: "center",
|
|
3392
|
+
gap: "0.5rem",
|
|
3393
|
+
color: "var(--nozle-foreground, var(--foreground))",
|
|
3394
|
+
fontSize: "0.875rem"
|
|
3395
|
+
},
|
|
3396
|
+
children: [
|
|
3397
|
+
/* @__PURE__ */ jsx(
|
|
3398
|
+
"span",
|
|
3399
|
+
{
|
|
3400
|
+
style: { color: "var(--nozle-primary, var(--primary))" },
|
|
3401
|
+
"aria-hidden": "true",
|
|
3402
|
+
children: "\u2713"
|
|
3403
|
+
}
|
|
3404
|
+
),
|
|
3405
|
+
feature
|
|
3406
|
+
]
|
|
3407
|
+
},
|
|
3408
|
+
feature
|
|
3409
|
+
))
|
|
1243
3410
|
}
|
|
1244
3411
|
),
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
3412
|
+
onSelect && /* @__PURE__ */ jsx(
|
|
3413
|
+
"button",
|
|
3414
|
+
{
|
|
3415
|
+
onClick: () => onSelect?.({
|
|
3416
|
+
planCode: id,
|
|
3417
|
+
returnUrl: returnUrl ?? window.location.href
|
|
3418
|
+
}),
|
|
3419
|
+
style: {
|
|
3420
|
+
background: isCurrent ? "var(--nozle-muted, var(--muted))" : "var(--nozle-primary, var(--primary))",
|
|
3421
|
+
color: isCurrent ? "var(--nozle-muted-foreground, var(--muted-foreground))" : "var(--nozle-primary-foreground, var(--primary-foreground))",
|
|
3422
|
+
border: "none",
|
|
3423
|
+
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
3424
|
+
padding: "0.75rem 1.5rem",
|
|
3425
|
+
cursor: isCurrent ? "default" : "pointer",
|
|
3426
|
+
fontWeight: 500,
|
|
3427
|
+
width: "100%"
|
|
3428
|
+
},
|
|
3429
|
+
disabled: isCurrent,
|
|
3430
|
+
"aria-label": isCurrent ? `Current plan: ${name}` : `Select ${name}`,
|
|
3431
|
+
children: isCurrent ? "Current Plan" : "Get Started"
|
|
3432
|
+
}
|
|
3433
|
+
)
|
|
3434
|
+
]
|
|
1248
3435
|
}
|
|
1249
3436
|
);
|
|
1250
3437
|
}
|
|
1251
|
-
function
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
function UpgradeModal({
|
|
1255
|
-
isOpen,
|
|
1256
|
-
planCode,
|
|
1257
|
-
returnUrl,
|
|
1258
|
-
preview,
|
|
1259
|
-
onStripeClientSecret,
|
|
1260
|
-
onCheckoutStarted,
|
|
1261
|
-
onCompleted,
|
|
1262
|
-
onScheduled,
|
|
1263
|
-
onConfirm,
|
|
1264
|
-
onCancel,
|
|
1265
|
-
onError
|
|
3438
|
+
function PlanComparison({
|
|
3439
|
+
plans,
|
|
3440
|
+
features
|
|
1266
3441
|
}) {
|
|
1267
|
-
const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
|
|
1268
|
-
const [processing, setProcessing] = useState(false);
|
|
1269
|
-
const [error, setError] = useState(null);
|
|
1270
|
-
const [confirming, setConfirming] = useState(false);
|
|
1271
|
-
async function handleConfirm() {
|
|
1272
|
-
setConfirming(true);
|
|
1273
|
-
setError(null);
|
|
1274
|
-
try {
|
|
1275
|
-
if (!createCheckout) {
|
|
1276
|
-
throw new Error("BillingProvider createCheckout callback is required for checkout");
|
|
1277
|
-
}
|
|
1278
|
-
const result = await createCheckout({
|
|
1279
|
-
planCode,
|
|
1280
|
-
returnUrl: returnUrl ?? window.location.href
|
|
1281
|
-
});
|
|
1282
|
-
await handleCheckoutResult(result, {
|
|
1283
|
-
verifyCheckout,
|
|
1284
|
-
getCheckoutStatus,
|
|
1285
|
-
onSuccess: () => {
|
|
1286
|
-
setProcessing(false);
|
|
1287
|
-
onCompleted?.();
|
|
1288
|
-
},
|
|
1289
|
-
onProcessing: () => setProcessing(true),
|
|
1290
|
-
onStripeClientSecret: (clientSecret) => {
|
|
1291
|
-
onStripeClientSecret?.(clientSecret);
|
|
1292
|
-
onCheckoutStarted?.();
|
|
1293
|
-
},
|
|
1294
|
-
onComplete: () => {
|
|
1295
|
-
setProcessing(false);
|
|
1296
|
-
onCompleted?.();
|
|
1297
|
-
},
|
|
1298
|
-
onScheduled: () => onScheduled?.()
|
|
1299
|
-
});
|
|
1300
|
-
onConfirm?.();
|
|
1301
|
-
} catch (cause) {
|
|
1302
|
-
const checkoutError = cause instanceof Error ? cause : new Error("Checkout failed");
|
|
1303
|
-
setError(checkoutError.message);
|
|
1304
|
-
onError?.(checkoutError);
|
|
1305
|
-
} finally {
|
|
1306
|
-
setConfirming(false);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
if (!isOpen) return null;
|
|
1310
3442
|
return /* @__PURE__ */ jsx(
|
|
1311
3443
|
"div",
|
|
1312
3444
|
{
|
|
1313
3445
|
style: {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
background: "oklch(0 0 0 / 0.5)",
|
|
1317
|
-
display: "flex",
|
|
1318
|
-
alignItems: "center",
|
|
1319
|
-
justifyContent: "center",
|
|
1320
|
-
zIndex: 1e3
|
|
3446
|
+
overflowX: "auto",
|
|
3447
|
+
background: "var(--nozle-background, var(--background))"
|
|
1321
3448
|
},
|
|
1322
|
-
role: "dialog",
|
|
1323
|
-
"aria-modal": "true",
|
|
1324
|
-
"aria-label": "Change plan",
|
|
1325
3449
|
children: /* @__PURE__ */ jsxs(
|
|
1326
|
-
"
|
|
3450
|
+
"table",
|
|
1327
3451
|
{
|
|
1328
3452
|
style: {
|
|
1329
|
-
background: "var(--nozle-background, var(--background))",
|
|
1330
|
-
color: "var(--nozle-foreground, var(--foreground))",
|
|
1331
|
-
border: "1px solid var(--nozle-border, var(--border))",
|
|
1332
|
-
borderRadius: "var(--nozle-radius, 0.5rem)",
|
|
1333
|
-
padding: "2rem",
|
|
1334
|
-
maxWidth: "28rem",
|
|
1335
3453
|
width: "100%",
|
|
1336
|
-
|
|
3454
|
+
borderCollapse: "collapse",
|
|
3455
|
+
fontSize: "0.875rem",
|
|
3456
|
+
color: "var(--nozle-foreground, var(--foreground))"
|
|
1337
3457
|
},
|
|
1338
3458
|
children: [
|
|
1339
|
-
/* @__PURE__ */ jsx("
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
preview && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
|
|
1343
|
-
/* @__PURE__ */ jsxs(
|
|
1344
|
-
"div",
|
|
3459
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
3460
|
+
/* @__PURE__ */ jsx(
|
|
3461
|
+
"th",
|
|
1345
3462
|
{
|
|
1346
3463
|
style: {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
gap: "0.5rem"
|
|
3464
|
+
padding: "0.75rem 1rem",
|
|
3465
|
+
textAlign: "left",
|
|
3466
|
+
borderBottom: "1px solid var(--nozle-border, var(--border))",
|
|
3467
|
+
color: "var(--nozle-muted-foreground, var(--muted-foreground))",
|
|
3468
|
+
fontWeight: 500
|
|
1353
3469
|
},
|
|
1354
|
-
children:
|
|
1355
|
-
preview.credit > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
1356
|
-
"Credits applied: -$",
|
|
1357
|
-
preview.credit.toFixed(2)
|
|
1358
|
-
] }),
|
|
1359
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1360
|
-
"New plan charge: $",
|
|
1361
|
-
preview.debit.toFixed(2)
|
|
1362
|
-
] }),
|
|
1363
|
-
/* @__PURE__ */ jsxs("strong", { children: [
|
|
1364
|
-
"Due today: $",
|
|
1365
|
-
preview.net.toFixed(2)
|
|
1366
|
-
] })
|
|
1367
|
-
]
|
|
3470
|
+
children: "Feature"
|
|
1368
3471
|
}
|
|
1369
3472
|
),
|
|
1370
|
-
/* @__PURE__ */
|
|
1371
|
-
"
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
3473
|
+
plans.map((plan) => /* @__PURE__ */ jsx(
|
|
3474
|
+
"th",
|
|
3475
|
+
{
|
|
3476
|
+
style: {
|
|
3477
|
+
padding: "0.75rem 1rem",
|
|
3478
|
+
textAlign: "center",
|
|
3479
|
+
borderBottom: "1px solid var(--nozle-border, var(--border))",
|
|
3480
|
+
fontWeight: 600,
|
|
3481
|
+
color: "var(--nozle-foreground, var(--foreground))"
|
|
3482
|
+
},
|
|
3483
|
+
children: plan.name
|
|
3484
|
+
},
|
|
3485
|
+
plan.id
|
|
3486
|
+
))
|
|
3487
|
+
] }) }),
|
|
3488
|
+
/* @__PURE__ */ jsx("tbody", { children: features.map((feature) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
3489
|
+
/* @__PURE__ */ jsx(
|
|
3490
|
+
"td",
|
|
3491
|
+
{
|
|
3492
|
+
style: {
|
|
3493
|
+
padding: "0.75rem 1rem",
|
|
3494
|
+
borderBottom: "1px solid var(--nozle-border, var(--border))",
|
|
3495
|
+
color: "var(--nozle-foreground, var(--foreground))"
|
|
3496
|
+
},
|
|
3497
|
+
children: feature.label
|
|
3498
|
+
}
|
|
3499
|
+
),
|
|
3500
|
+
plans.map((plan) => {
|
|
3501
|
+
const value = feature.values[plan.id];
|
|
3502
|
+
return /* @__PURE__ */ jsx(
|
|
3503
|
+
"td",
|
|
3504
|
+
{
|
|
3505
|
+
style: {
|
|
3506
|
+
padding: "0.75rem 1rem",
|
|
3507
|
+
textAlign: "center",
|
|
3508
|
+
borderBottom: "1px solid var(--nozle-border, var(--border))"
|
|
3509
|
+
},
|
|
3510
|
+
children: typeof value === "boolean" ? value ? /* @__PURE__ */ jsx(
|
|
3511
|
+
"span",
|
|
3512
|
+
{
|
|
3513
|
+
style: {
|
|
3514
|
+
color: "var(--nozle-primary, var(--primary))"
|
|
3515
|
+
},
|
|
3516
|
+
"aria-label": "Included",
|
|
3517
|
+
children: "\u2713"
|
|
3518
|
+
}
|
|
3519
|
+
) : /* @__PURE__ */ jsx(
|
|
3520
|
+
"span",
|
|
3521
|
+
{
|
|
3522
|
+
style: {
|
|
3523
|
+
color: "var(--nozle-muted-foreground, var(--muted-foreground))"
|
|
3524
|
+
},
|
|
3525
|
+
"aria-label": "Not included",
|
|
3526
|
+
children: "\u2715"
|
|
3527
|
+
}
|
|
3528
|
+
) : /* @__PURE__ */ jsx("span", { children: value ?? "\u2014" })
|
|
3529
|
+
},
|
|
3530
|
+
plan.id
|
|
3531
|
+
);
|
|
3532
|
+
})
|
|
3533
|
+
] }, feature.key)) })
|
|
1380
3534
|
]
|
|
1381
3535
|
}
|
|
1382
3536
|
)
|
|
@@ -1386,6 +3540,9 @@ function UpgradeModal({
|
|
|
1386
3540
|
function UpgradeButton({
|
|
1387
3541
|
planCode,
|
|
1388
3542
|
returnUrl,
|
|
3543
|
+
subscriptionId,
|
|
3544
|
+
quoteId,
|
|
3545
|
+
idempotencyKey,
|
|
1389
3546
|
preview,
|
|
1390
3547
|
label = "Upgrade",
|
|
1391
3548
|
className,
|
|
@@ -1423,6 +3580,9 @@ function UpgradeButton({
|
|
|
1423
3580
|
isOpen,
|
|
1424
3581
|
planCode,
|
|
1425
3582
|
returnUrl,
|
|
3583
|
+
subscriptionId,
|
|
3584
|
+
quoteId,
|
|
3585
|
+
idempotencyKey,
|
|
1426
3586
|
preview,
|
|
1427
3587
|
onStripeClientSecret,
|
|
1428
3588
|
onCheckoutStarted: () => {
|
|
@@ -1477,10 +3637,10 @@ function PlanGate({
|
|
|
1477
3637
|
}
|
|
1478
3638
|
function UpgradePrompt({
|
|
1479
3639
|
text = "Upgrade to access this feature",
|
|
1480
|
-
planName,
|
|
3640
|
+
planName: planName2,
|
|
1481
3641
|
onUpgrade
|
|
1482
3642
|
}) {
|
|
1483
|
-
const displayText =
|
|
3643
|
+
const displayText = planName2 ? `Upgrade to ${planName2} to access this feature` : text;
|
|
1484
3644
|
function handleUpgrade() {
|
|
1485
3645
|
if (onUpgrade) {
|
|
1486
3646
|
onUpgrade();
|
|
@@ -1861,6 +4021,6 @@ function PaymentMethodDisplay({
|
|
|
1861
4021
|
] });
|
|
1862
4022
|
}
|
|
1863
4023
|
|
|
1864
|
-
export { BillingContext, BillingProvider, Checkout, CheckoutButton, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanComparison, PlanGate, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageDashboard, UsageGate, UsageMeter, getUsageMeterColor, useBillingContext,
|
|
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 };
|
|
1865
4025
|
//# sourceMappingURL=index.js.map
|
|
1866
4026
|
//# sourceMappingURL=index.js.map
|