@nozle-js/react 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { createContext, useMemo, useContext, useState, useEffect, useCallback } from 'react';
1
+ import React, { createContext, useMemo, useContext, useState, useEffect, useCallback, useRef } from 'react';
2
2
  import { jsxs, Fragment, jsx } 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';
@@ -25,21 +25,29 @@ var BillingContext = createContext(null);
25
25
  function BillingProvider({
26
26
  publishableKey,
27
27
  createCheckout,
28
- baseUrl = "https://api.nozle.app",
28
+ verifyCheckout,
29
+ getCheckoutStatus,
30
+ baseUrl = "https://api.nozle.app/engine",
29
31
  children
30
32
  }) {
31
33
  if (!publishableKey.startsWith("pk_")) {
32
- throw new Error("BillingProvider publishableKey must be a publishable key (pk_)");
34
+ throw new Error(
35
+ "BillingProvider publishableKey must be a publishable key (pk_)"
36
+ );
33
37
  }
34
38
  const client = useMemo(
35
39
  () => createClient(publishableKey, baseUrl),
36
40
  [publishableKey, baseUrl]
37
41
  );
38
42
  const contextValue = useMemo(
39
- () => ({ client, createCheckout }),
40
- [client, createCheckout]
43
+ () => ({ client, createCheckout, verifyCheckout, getCheckoutStatus }),
44
+ [client, createCheckout, verifyCheckout, getCheckoutStatus]
45
+ );
46
+ return React.createElement(
47
+ BillingContext.Provider,
48
+ { value: contextValue },
49
+ children
41
50
  );
42
- return React.createElement(BillingContext.Provider, { value: contextValue }, children);
43
51
  }
44
52
  function useOptionalBillingContext() {
45
53
  return useContext(BillingContext);
@@ -110,7 +118,11 @@ function useCheckout() {
110
118
  navigateToCheckout(result.url);
111
119
  return null;
112
120
  }
113
- if ("type" in result && (result.type === "completed" || result.type === "scheduled")) {
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)) {
114
126
  return null;
115
127
  }
116
128
  throw new Error("Checkout response did not include a supported result type");
@@ -126,20 +138,151 @@ function useCheckout() {
126
138
  );
127
139
  return { fetchClientSecret, checkout, isLoading, error };
128
140
  }
141
+
142
+ // src/components/billing/razorpay-checkout.ts
143
+ var loadingScript;
129
144
  function loadRazorpayScript() {
130
- return new Promise((resolve, reject) => {
131
- if (document.getElementById("razorpay-js")) {
132
- resolve();
133
- return;
145
+ if (typeof window === "undefined")
146
+ return Promise.reject(new Error("Razorpay checkout requires a browser"));
147
+ if (window.Razorpay) return Promise.resolve();
148
+ if (loadingScript) return loadingScript;
149
+ loadingScript = new Promise((resolve, reject) => {
150
+ const existing = document.querySelector(
151
+ 'script[src="https://checkout.razorpay.com/v1/checkout.js"]'
152
+ );
153
+ const script = existing || document.createElement("script");
154
+ const finish = (error) => {
155
+ clearTimeout(timer);
156
+ script.removeEventListener("load", onLoad);
157
+ script.removeEventListener("error", onError);
158
+ if (error) {
159
+ script.remove();
160
+ reject(error);
161
+ } else {
162
+ resolve();
163
+ }
164
+ };
165
+ const onLoad = () => finish(
166
+ window.Razorpay ? void 0 : new Error("Razorpay checkout did not initialize")
167
+ );
168
+ const onError = () => finish(new Error("Failed to load Razorpay checkout"));
169
+ const timer = setTimeout(
170
+ () => finish(new Error("Razorpay checkout timed out while loading")),
171
+ 15e3
172
+ );
173
+ script.addEventListener("load", onLoad);
174
+ script.addEventListener("error", onError);
175
+ if (!existing) {
176
+ script.id = "razorpay-js";
177
+ script.src = "https://checkout.razorpay.com/v1/checkout.js";
178
+ document.body.appendChild(script);
134
179
  }
135
- const script = document.createElement("script");
136
- script.id = "razorpay-js";
137
- script.src = "https://checkout.razorpay.com/v1/checkout.js";
138
- script.onload = () => resolve();
139
- script.onerror = () => reject(new Error("Failed to load Razorpay.js"));
140
- document.body.appendChild(script);
180
+ }).catch((error) => {
181
+ loadingScript = void 0;
182
+ throw error;
183
+ });
184
+ return loadingScript;
185
+ }
186
+ function checkStatus(status, id) {
187
+ if (status.checkout_id !== id || status.provider !== "razorpay")
188
+ throw new Error("Checkout status did not match this checkout");
189
+ if (["failed", "expired", "needs_review"].includes(status.status))
190
+ throw new Error(`Checkout ${status.status.replaceAll("_", " ")}`);
191
+ }
192
+ async function waitForCheckout(id, actions, initial, phase = "confirm") {
193
+ let status = initial;
194
+ if (!status && !actions.getCheckoutStatus)
195
+ throw new Error("BillingProvider getCheckoutStatus callback is required");
196
+ for (let attempt = 0; attempt < 30; attempt++) {
197
+ status = status || await actions.getCheckoutStatus(id);
198
+ checkStatus(status, id);
199
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded" || phase === "prepare" && status.status === "awaiting_payment")
200
+ return status;
201
+ actions.onProcessing?.(status);
202
+ if (!actions.getCheckoutStatus) return status;
203
+ if (attempt < 29) {
204
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
205
+ status = void 0;
206
+ }
207
+ }
208
+ return status;
209
+ }
210
+ async function openRazorpayCheckout(result, actions) {
211
+ if (!result.checkout_id || !result.key_id || !result.order_id)
212
+ throw new Error(
213
+ "Razorpay checkout requires server-issued checkout_id, key_id and order_id"
214
+ );
215
+ if (!actions.verifyCheckout)
216
+ throw new Error(
217
+ "BillingProvider verifyCheckout callback is required for Razorpay checkout"
218
+ );
219
+ await loadRazorpayScript();
220
+ await new Promise((resolve, reject) => {
221
+ let finishing = false;
222
+ const checkout = new window.Razorpay({
223
+ key: result.key_id,
224
+ order_id: result.order_id,
225
+ amount: result.amount_cents,
226
+ currency: result.currency,
227
+ ...result.recurring && {
228
+ customer_id: result.customer_id,
229
+ recurring: "1"
230
+ },
231
+ handler: (response) => {
232
+ if (finishing) return;
233
+ finishing = true;
234
+ void (async () => {
235
+ const verified = await actions.verifyCheckout(
236
+ result.checkout_id,
237
+ response
238
+ );
239
+ const status = await waitForCheckout(
240
+ result.checkout_id,
241
+ actions,
242
+ verified
243
+ );
244
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded")
245
+ actions.onSuccess?.(response.razorpay_payment_id);
246
+ else actions.onProcessing?.(status);
247
+ resolve();
248
+ })().catch(reject);
249
+ },
250
+ modal: {
251
+ ondismiss: () => {
252
+ if (!finishing) {
253
+ finishing = true;
254
+ actions.onDismiss?.();
255
+ resolve();
256
+ }
257
+ }
258
+ }
259
+ });
260
+ checkout.on("payment.failed", () => {
261
+ if (finishing) return;
262
+ finishing = true;
263
+ checkout.close();
264
+ reject(new Error("Payment failed. You can retry checkout."));
265
+ });
266
+ checkout.open();
141
267
  });
142
268
  }
269
+ async function resumeCheckout(result, actions) {
270
+ const status = await waitForCheckout(
271
+ result.checkout_id,
272
+ actions,
273
+ void 0,
274
+ "prepare"
275
+ );
276
+ if (status.status === "awaiting_payment") return status.checkout;
277
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded")
278
+ return {
279
+ type: "completed",
280
+ status: "succeeded",
281
+ invoice_id: status.invoice_id || void 0
282
+ };
283
+ actions.onProcessing?.(status);
284
+ return void 0;
285
+ }
143
286
  async function handleCheckoutResult(result, options) {
144
287
  if ("type" in result && result.type === "scheduled") {
145
288
  options.onScheduled?.(result);
@@ -150,14 +293,16 @@ async function handleCheckoutResult(result, options) {
150
293
  return;
151
294
  }
152
295
  if ("type" in result && result.type === "razorpay") {
153
- if (!options.razorpayKeyId) throw new Error("razorpayKeyId is required for Razorpay checkout");
154
- await loadRazorpayScript();
155
- const checkout = new window.Razorpay({
156
- key: options.razorpayKeyId,
157
- order_id: result.orderId,
158
- handler: (response) => options.onSuccess?.(response.razorpay_payment_id)
159
- });
160
- checkout.open();
296
+ await openRazorpayCheckout(result, options);
297
+ return;
298
+ }
299
+ if ("type" in result && result.type === "processing") {
300
+ const next = await resumeCheckout(result, options);
301
+ if (next) await handleCheckoutResult(next, options);
302
+ return;
303
+ }
304
+ if ("type" in result && result.type === "hosted") {
305
+ window.location.assign(result.payment_url);
161
306
  return;
162
307
  }
163
308
  if ("url" in result && result.url) {
@@ -170,7 +315,10 @@ async function handleCheckoutResult(result, options) {
170
315
  options.onStripeClientSecret(clientSecret);
171
316
  return;
172
317
  }
173
- if (clientSecret) throw new Error("onStripeClientSecret is required for embedded Stripe checkout");
318
+ if (clientSecret)
319
+ throw new Error(
320
+ "onStripeClientSecret is required for embedded Stripe checkout"
321
+ );
174
322
  throw new Error("Stripe checkout did not include a URL or client secret");
175
323
  }
176
324
  throw new Error("Unknown checkout response format");
@@ -183,30 +331,58 @@ function CheckoutButton({
183
331
  style,
184
332
  onError,
185
333
  razorpayKeyId,
334
+ registerMandate,
335
+ onProcessing,
336
+ onDismiss,
186
337
  onStripeClientSecret,
187
338
  onSuccess,
188
339
  onComplete,
189
340
  onScheduled
190
341
  }) {
191
- const { createCheckout } = useBillingContext();
342
+ const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
343
+ const attempt = useRef(void 0);
344
+ const inFlight = useRef(false);
345
+ const [processing, setProcessing] = useState(false);
192
346
  const [loading, setLoading] = useState(false);
193
347
  const [error, setError] = useState(null);
194
348
  async function handleClick() {
349
+ if (inFlight.current) return;
350
+ inFlight.current = true;
195
351
  setLoading(true);
352
+ setProcessing(false);
196
353
  setError(null);
197
354
  try {
198
355
  if (!createCheckout) {
199
- throw new Error("BillingProvider createCheckout callback is required for checkout");
356
+ throw new Error(
357
+ "BillingProvider createCheckout callback is required for checkout"
358
+ );
200
359
  }
360
+ if (attempt.current?.plan !== planCode)
361
+ attempt.current = { plan: planCode, key: crypto.randomUUID() };
201
362
  const result = await createCheckout({
363
+ idempotencyKey: attempt.current.key,
364
+ ...registerMandate !== void 0 && { registerMandate },
202
365
  planCode,
203
366
  returnUrl: returnUrl ?? window.location.href
204
367
  });
205
368
  await handleCheckoutResult(result, {
206
369
  razorpayKeyId,
370
+ verifyCheckout,
371
+ getCheckoutStatus,
372
+ onProcessing: (status) => {
373
+ setProcessing(true);
374
+ onProcessing?.(status);
375
+ },
376
+ onDismiss,
207
377
  onStripeClientSecret,
208
- onSuccess,
209
- onComplete,
378
+ onSuccess: (id) => {
379
+ setProcessing(false);
380
+ onSuccess?.(id);
381
+ },
382
+ onComplete: (completed) => {
383
+ setProcessing(false);
384
+ onComplete?.(completed);
385
+ },
210
386
  onScheduled
211
387
  });
212
388
  } catch (cause) {
@@ -214,6 +390,7 @@ function CheckoutButton({
214
390
  setError(checkoutError);
215
391
  onError?.(checkoutError);
216
392
  } finally {
393
+ inFlight.current = false;
217
394
  setLoading(false);
218
395
  }
219
396
  }
@@ -240,6 +417,7 @@ function CheckoutButton({
240
417
  children: loading ? "Loading..." : label
241
418
  }
242
419
  ),
420
+ processing && /* @__PURE__ */ jsx("span", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
243
421
  error && /* @__PURE__ */ jsx("span", { role: "alert", children: error.message })
244
422
  ] });
245
423
  }
@@ -512,6 +690,7 @@ function PricingTable({
512
690
  const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
513
691
  const [isAnnual, setIsAnnual] = useState(false);
514
692
  const [loading, setLoading] = useState(!propPlans);
693
+ const [processing, setProcessing] = useState(false);
515
694
  const [checkoutError, setCheckoutError] = useState(null);
516
695
  useEffect(() => {
517
696
  if (propPlans) {
@@ -554,7 +733,14 @@ function PricingTable({
554
733
  returnUrl: returnUrl ?? window.location.href
555
734
  });
556
735
  onCheckoutResult?.(result);
557
- await handleCheckoutResult(result, { onStripeClientSecret });
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)
743
+ });
558
744
  } catch (cause) {
559
745
  const error = cause instanceof Error ? cause : new Error("Checkout failed");
560
746
  setCheckoutError(error.message);
@@ -570,6 +756,7 @@ function PricingTable({
570
756
  background: C.bg
571
757
  }, children: [
572
758
  showToggle && hasAnnual && /* @__PURE__ */ jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
759
+ processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
573
760
  checkoutError && /* @__PURE__ */ jsx("p", { role: "alert", children: checkoutError }),
574
761
  /* @__PURE__ */ jsx("div", { style: {
575
762
  display: "grid",
@@ -923,7 +1110,16 @@ function CheckoutInner({
923
1110
  }
924
1111
  }
925
1112
  ),
926
- error !== null && /* @__PURE__ */ jsx("p", { style: { color: "var(--nozle-destructive, #dc2626)", margin: "0.5rem 0 0" }, children: error.message }),
1113
+ error !== null && /* @__PURE__ */ jsx(
1114
+ "p",
1115
+ {
1116
+ style: {
1117
+ color: "var(--nozle-destructive, #dc2626)",
1118
+ margin: "0.5rem 0 0"
1119
+ },
1120
+ children: error.message
1121
+ }
1122
+ ),
927
1123
  children ?? /* @__PURE__ */ jsx(
928
1124
  "button",
929
1125
  {
@@ -948,7 +1144,7 @@ function CheckoutInner({
948
1144
  )
949
1145
  ] }) });
950
1146
  }
951
- function Checkout({
1147
+ function StripeCheckout({
952
1148
  clientSecret,
953
1149
  publishableKey,
954
1150
  stripeAccount,
@@ -972,7 +1168,14 @@ function Checkout({
972
1168
  [clientSecret, onComplete]
973
1169
  );
974
1170
  if (clientSecret.startsWith("cs_")) {
975
- return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(EmbeddedCheckoutProvider, { stripe: stripePromise, options: embeddedOptions, children: /* @__PURE__ */ jsx(EmbeddedCheckout, {}) }) });
1171
+ return /* @__PURE__ */ jsx("div", { className, style, children: /* @__PURE__ */ jsx(
1172
+ EmbeddedCheckoutProvider,
1173
+ {
1174
+ stripe: stripePromise,
1175
+ options: embeddedOptions,
1176
+ children: /* @__PURE__ */ jsx(EmbeddedCheckout, {})
1177
+ }
1178
+ ) });
976
1179
  }
977
1180
  const options = { clientSecret, appearance };
978
1181
  return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsx(
@@ -989,6 +1192,65 @@ function Checkout({
989
1192
  }
990
1193
  ) }, clientSecret);
991
1194
  }
1195
+ function ProviderCheckout(props) {
1196
+ const billing = useOptionalBillingContext();
1197
+ const [busy, setBusy] = useState(false);
1198
+ const [error, setError] = useState(null);
1199
+ const [processing, setProcessing] = useState(false);
1200
+ const pay = async () => {
1201
+ if (busy) return;
1202
+ setBusy(true);
1203
+ setError(null);
1204
+ try {
1205
+ await handleCheckoutResult(props.checkout, {
1206
+ verifyCheckout: billing?.verifyCheckout,
1207
+ getCheckoutStatus: billing?.getCheckoutStatus,
1208
+ onSuccess: (id) => {
1209
+ setProcessing(false);
1210
+ props.onSuccess?.(id);
1211
+ props.onComplete?.();
1212
+ },
1213
+ onComplete: () => {
1214
+ setProcessing(false);
1215
+ props.onComplete?.();
1216
+ },
1217
+ onProcessing: (status) => {
1218
+ setProcessing(true);
1219
+ props.onProcessing?.(status);
1220
+ }
1221
+ });
1222
+ } catch (cause) {
1223
+ const failure = cause instanceof Error ? cause : new Error("Checkout failed");
1224
+ setError(failure);
1225
+ props.onError?.(failure);
1226
+ } finally {
1227
+ setBusy(false);
1228
+ }
1229
+ };
1230
+ return /* @__PURE__ */ jsx(
1231
+ CheckoutContext.Provider,
1232
+ {
1233
+ value: { confirmPayment: pay, isProcessing: busy, error },
1234
+ children: /* @__PURE__ */ jsxs("div", { className: props.className, children: [
1235
+ /* @__PURE__ */ jsx(
1236
+ "button",
1237
+ {
1238
+ type: "button",
1239
+ disabled: busy,
1240
+ "aria-busy": busy,
1241
+ onClick: () => void pay(),
1242
+ children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
1243
+ }
1244
+ ),
1245
+ processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1246
+ error && /* @__PURE__ */ jsx("p", { role: "alert", children: error.message })
1247
+ ] })
1248
+ }
1249
+ );
1250
+ }
1251
+ function Checkout(props) {
1252
+ return "checkout" in props ? /* @__PURE__ */ jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsx(StripeCheckout, { ...props });
1253
+ }
992
1254
  function UpgradeModal({
993
1255
  isOpen,
994
1256
  planCode,
@@ -1002,7 +1264,8 @@ function UpgradeModal({
1002
1264
  onCancel,
1003
1265
  onError
1004
1266
  }) {
1005
- const { createCheckout } = useBillingContext();
1267
+ const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
1268
+ const [processing, setProcessing] = useState(false);
1006
1269
  const [error, setError] = useState(null);
1007
1270
  const [confirming, setConfirming] = useState(false);
1008
1271
  async function handleConfirm() {
@@ -1017,11 +1280,21 @@ function UpgradeModal({
1017
1280
  returnUrl: returnUrl ?? window.location.href
1018
1281
  });
1019
1282
  await handleCheckoutResult(result, {
1283
+ verifyCheckout,
1284
+ getCheckoutStatus,
1285
+ onSuccess: () => {
1286
+ setProcessing(false);
1287
+ onCompleted?.();
1288
+ },
1289
+ onProcessing: () => setProcessing(true),
1020
1290
  onStripeClientSecret: (clientSecret) => {
1021
1291
  onStripeClientSecret?.(clientSecret);
1022
1292
  onCheckoutStarted?.();
1023
1293
  },
1024
- onComplete: () => onCompleted?.(),
1294
+ onComplete: () => {
1295
+ setProcessing(false);
1296
+ onCompleted?.();
1297
+ },
1025
1298
  onScheduled: () => onScheduled?.()
1026
1299
  });
1027
1300
  onConfirm?.();
@@ -1064,6 +1337,7 @@ function UpgradeModal({
1064
1337
  },
1065
1338
  children: [
1066
1339
  /* @__PURE__ */ jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" }, children: "Confirm plan change" }),
1340
+ processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1067
1341
  error && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: "oklch(0.6 0.2 25)" }, children: error }),
1068
1342
  preview && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1069
1343
  /* @__PURE__ */ jsxs(