@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.cjs CHANGED
@@ -31,21 +31,29 @@ var BillingContext = React.createContext(null);
31
31
  function BillingProvider({
32
32
  publishableKey,
33
33
  createCheckout,
34
- baseUrl = "https://api.nozle.app",
34
+ verifyCheckout,
35
+ getCheckoutStatus,
36
+ baseUrl = "https://api.nozle.app/engine",
35
37
  children
36
38
  }) {
37
39
  if (!publishableKey.startsWith("pk_")) {
38
- throw new Error("BillingProvider publishableKey must be a publishable key (pk_)");
40
+ throw new Error(
41
+ "BillingProvider publishableKey must be a publishable key (pk_)"
42
+ );
39
43
  }
40
44
  const client = React.useMemo(
41
45
  () => createClient(publishableKey, baseUrl),
42
46
  [publishableKey, baseUrl]
43
47
  );
44
48
  const contextValue = React.useMemo(
45
- () => ({ client, createCheckout }),
46
- [client, createCheckout]
49
+ () => ({ client, createCheckout, verifyCheckout, getCheckoutStatus }),
50
+ [client, createCheckout, verifyCheckout, getCheckoutStatus]
51
+ );
52
+ return React__default.default.createElement(
53
+ BillingContext.Provider,
54
+ { value: contextValue },
55
+ children
47
56
  );
48
- return React__default.default.createElement(BillingContext.Provider, { value: contextValue }, children);
49
57
  }
50
58
  function useOptionalBillingContext() {
51
59
  return React.useContext(BillingContext);
@@ -116,7 +124,11 @@ function useCheckout() {
116
124
  navigateToCheckout(result.url);
117
125
  return null;
118
126
  }
119
- if ("type" in result && (result.type === "completed" || result.type === "scheduled")) {
127
+ if ("type" in result && result.type === "hosted") {
128
+ navigateToCheckout(result.payment_url);
129
+ return null;
130
+ }
131
+ if ("type" in result && ["completed", "scheduled", "razorpay", "processing"].includes(result.type)) {
120
132
  return null;
121
133
  }
122
134
  throw new Error("Checkout response did not include a supported result type");
@@ -132,20 +144,151 @@ function useCheckout() {
132
144
  );
133
145
  return { fetchClientSecret, checkout, isLoading, error };
134
146
  }
147
+
148
+ // src/components/billing/razorpay-checkout.ts
149
+ var loadingScript;
135
150
  function loadRazorpayScript() {
136
- return new Promise((resolve, reject) => {
137
- if (document.getElementById("razorpay-js")) {
138
- resolve();
139
- return;
151
+ if (typeof window === "undefined")
152
+ return Promise.reject(new Error("Razorpay checkout requires a browser"));
153
+ if (window.Razorpay) return Promise.resolve();
154
+ if (loadingScript) return loadingScript;
155
+ loadingScript = new Promise((resolve, reject) => {
156
+ const existing = document.querySelector(
157
+ 'script[src="https://checkout.razorpay.com/v1/checkout.js"]'
158
+ );
159
+ const script = existing || document.createElement("script");
160
+ const finish = (error) => {
161
+ clearTimeout(timer);
162
+ script.removeEventListener("load", onLoad);
163
+ script.removeEventListener("error", onError);
164
+ if (error) {
165
+ script.remove();
166
+ reject(error);
167
+ } else {
168
+ resolve();
169
+ }
170
+ };
171
+ const onLoad = () => finish(
172
+ window.Razorpay ? void 0 : new Error("Razorpay checkout did not initialize")
173
+ );
174
+ const onError = () => finish(new Error("Failed to load Razorpay checkout"));
175
+ const timer = setTimeout(
176
+ () => finish(new Error("Razorpay checkout timed out while loading")),
177
+ 15e3
178
+ );
179
+ script.addEventListener("load", onLoad);
180
+ script.addEventListener("error", onError);
181
+ if (!existing) {
182
+ script.id = "razorpay-js";
183
+ script.src = "https://checkout.razorpay.com/v1/checkout.js";
184
+ document.body.appendChild(script);
140
185
  }
141
- const script = document.createElement("script");
142
- script.id = "razorpay-js";
143
- script.src = "https://checkout.razorpay.com/v1/checkout.js";
144
- script.onload = () => resolve();
145
- script.onerror = () => reject(new Error("Failed to load Razorpay.js"));
146
- document.body.appendChild(script);
186
+ }).catch((error) => {
187
+ loadingScript = void 0;
188
+ throw error;
189
+ });
190
+ return loadingScript;
191
+ }
192
+ function checkStatus(status, id) {
193
+ if (status.checkout_id !== id || status.provider !== "razorpay")
194
+ throw new Error("Checkout status did not match this checkout");
195
+ if (["failed", "expired", "needs_review"].includes(status.status))
196
+ throw new Error(`Checkout ${status.status.replaceAll("_", " ")}`);
197
+ }
198
+ async function waitForCheckout(id, actions, initial, phase = "confirm") {
199
+ let status = initial;
200
+ if (!status && !actions.getCheckoutStatus)
201
+ throw new Error("BillingProvider getCheckoutStatus callback is required");
202
+ for (let attempt = 0; attempt < 30; attempt++) {
203
+ status = status || await actions.getCheckoutStatus(id);
204
+ checkStatus(status, id);
205
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded" || phase === "prepare" && status.status === "awaiting_payment")
206
+ return status;
207
+ actions.onProcessing?.(status);
208
+ if (!actions.getCheckoutStatus) return status;
209
+ if (attempt < 29) {
210
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
211
+ status = void 0;
212
+ }
213
+ }
214
+ return status;
215
+ }
216
+ async function openRazorpayCheckout(result, actions) {
217
+ if (!result.checkout_id || !result.key_id || !result.order_id)
218
+ throw new Error(
219
+ "Razorpay checkout requires server-issued checkout_id, key_id and order_id"
220
+ );
221
+ if (!actions.verifyCheckout)
222
+ throw new Error(
223
+ "BillingProvider verifyCheckout callback is required for Razorpay checkout"
224
+ );
225
+ await loadRazorpayScript();
226
+ await new Promise((resolve, reject) => {
227
+ let finishing = false;
228
+ const checkout = new window.Razorpay({
229
+ key: result.key_id,
230
+ order_id: result.order_id,
231
+ amount: result.amount_cents,
232
+ currency: result.currency,
233
+ ...result.recurring && {
234
+ customer_id: result.customer_id,
235
+ recurring: "1"
236
+ },
237
+ handler: (response) => {
238
+ if (finishing) return;
239
+ finishing = true;
240
+ void (async () => {
241
+ const verified = await actions.verifyCheckout(
242
+ result.checkout_id,
243
+ response
244
+ );
245
+ const status = await waitForCheckout(
246
+ result.checkout_id,
247
+ actions,
248
+ verified
249
+ );
250
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded")
251
+ actions.onSuccess?.(response.razorpay_payment_id);
252
+ else actions.onProcessing?.(status);
253
+ resolve();
254
+ })().catch(reject);
255
+ },
256
+ modal: {
257
+ ondismiss: () => {
258
+ if (!finishing) {
259
+ finishing = true;
260
+ actions.onDismiss?.();
261
+ resolve();
262
+ }
263
+ }
264
+ }
265
+ });
266
+ checkout.on("payment.failed", () => {
267
+ if (finishing) return;
268
+ finishing = true;
269
+ checkout.close();
270
+ reject(new Error("Payment failed. You can retry checkout."));
271
+ });
272
+ checkout.open();
147
273
  });
148
274
  }
275
+ async function resumeCheckout(result, actions) {
276
+ const status = await waitForCheckout(
277
+ result.checkout_id,
278
+ actions,
279
+ void 0,
280
+ "prepare"
281
+ );
282
+ if (status.status === "awaiting_payment") return status.checkout;
283
+ if (status.status === "succeeded" && status.fulfillment_status === "succeeded")
284
+ return {
285
+ type: "completed",
286
+ status: "succeeded",
287
+ invoice_id: status.invoice_id || void 0
288
+ };
289
+ actions.onProcessing?.(status);
290
+ return void 0;
291
+ }
149
292
  async function handleCheckoutResult(result, options) {
150
293
  if ("type" in result && result.type === "scheduled") {
151
294
  options.onScheduled?.(result);
@@ -156,14 +299,16 @@ async function handleCheckoutResult(result, options) {
156
299
  return;
157
300
  }
158
301
  if ("type" in result && result.type === "razorpay") {
159
- if (!options.razorpayKeyId) throw new Error("razorpayKeyId is required for Razorpay checkout");
160
- await loadRazorpayScript();
161
- const checkout = new window.Razorpay({
162
- key: options.razorpayKeyId,
163
- order_id: result.orderId,
164
- handler: (response) => options.onSuccess?.(response.razorpay_payment_id)
165
- });
166
- checkout.open();
302
+ await openRazorpayCheckout(result, options);
303
+ return;
304
+ }
305
+ if ("type" in result && result.type === "processing") {
306
+ const next = await resumeCheckout(result, options);
307
+ if (next) await handleCheckoutResult(next, options);
308
+ return;
309
+ }
310
+ if ("type" in result && result.type === "hosted") {
311
+ window.location.assign(result.payment_url);
167
312
  return;
168
313
  }
169
314
  if ("url" in result && result.url) {
@@ -176,7 +321,10 @@ async function handleCheckoutResult(result, options) {
176
321
  options.onStripeClientSecret(clientSecret);
177
322
  return;
178
323
  }
179
- if (clientSecret) throw new Error("onStripeClientSecret is required for embedded Stripe checkout");
324
+ if (clientSecret)
325
+ throw new Error(
326
+ "onStripeClientSecret is required for embedded Stripe checkout"
327
+ );
180
328
  throw new Error("Stripe checkout did not include a URL or client secret");
181
329
  }
182
330
  throw new Error("Unknown checkout response format");
@@ -189,30 +337,58 @@ function CheckoutButton({
189
337
  style,
190
338
  onError,
191
339
  razorpayKeyId,
340
+ registerMandate,
341
+ onProcessing,
342
+ onDismiss,
192
343
  onStripeClientSecret,
193
344
  onSuccess,
194
345
  onComplete,
195
346
  onScheduled
196
347
  }) {
197
- const { createCheckout } = useBillingContext();
348
+ const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
349
+ const attempt = React.useRef(void 0);
350
+ const inFlight = React.useRef(false);
351
+ const [processing, setProcessing] = React.useState(false);
198
352
  const [loading, setLoading] = React.useState(false);
199
353
  const [error, setError] = React.useState(null);
200
354
  async function handleClick() {
355
+ if (inFlight.current) return;
356
+ inFlight.current = true;
201
357
  setLoading(true);
358
+ setProcessing(false);
202
359
  setError(null);
203
360
  try {
204
361
  if (!createCheckout) {
205
- throw new Error("BillingProvider createCheckout callback is required for checkout");
362
+ throw new Error(
363
+ "BillingProvider createCheckout callback is required for checkout"
364
+ );
206
365
  }
366
+ if (attempt.current?.plan !== planCode)
367
+ attempt.current = { plan: planCode, key: crypto.randomUUID() };
207
368
  const result = await createCheckout({
369
+ idempotencyKey: attempt.current.key,
370
+ ...registerMandate !== void 0 && { registerMandate },
208
371
  planCode,
209
372
  returnUrl: returnUrl ?? window.location.href
210
373
  });
211
374
  await handleCheckoutResult(result, {
212
375
  razorpayKeyId,
376
+ verifyCheckout,
377
+ getCheckoutStatus,
378
+ onProcessing: (status) => {
379
+ setProcessing(true);
380
+ onProcessing?.(status);
381
+ },
382
+ onDismiss,
213
383
  onStripeClientSecret,
214
- onSuccess,
215
- onComplete,
384
+ onSuccess: (id) => {
385
+ setProcessing(false);
386
+ onSuccess?.(id);
387
+ },
388
+ onComplete: (completed) => {
389
+ setProcessing(false);
390
+ onComplete?.(completed);
391
+ },
216
392
  onScheduled
217
393
  });
218
394
  } catch (cause) {
@@ -220,6 +396,7 @@ function CheckoutButton({
220
396
  setError(checkoutError);
221
397
  onError?.(checkoutError);
222
398
  } finally {
399
+ inFlight.current = false;
223
400
  setLoading(false);
224
401
  }
225
402
  }
@@ -246,6 +423,7 @@ function CheckoutButton({
246
423
  children: loading ? "Loading..." : label
247
424
  }
248
425
  ),
426
+ processing && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
249
427
  error && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "alert", children: error.message })
250
428
  ] });
251
429
  }
@@ -518,6 +696,7 @@ function PricingTable({
518
696
  const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
519
697
  const [isAnnual, setIsAnnual] = React.useState(false);
520
698
  const [loading, setLoading] = React.useState(!propPlans);
699
+ const [processing, setProcessing] = React.useState(false);
521
700
  const [checkoutError, setCheckoutError] = React.useState(null);
522
701
  React.useEffect(() => {
523
702
  if (propPlans) {
@@ -560,7 +739,14 @@ function PricingTable({
560
739
  returnUrl: returnUrl ?? window.location.href
561
740
  });
562
741
  onCheckoutResult?.(result);
563
- await handleCheckoutResult(result, { onStripeClientSecret });
742
+ await handleCheckoutResult(result, {
743
+ onStripeClientSecret,
744
+ verifyCheckout: billing?.verifyCheckout,
745
+ getCheckoutStatus: billing?.getCheckoutStatus,
746
+ onProcessing: () => setProcessing(true),
747
+ onSuccess: () => setProcessing(false),
748
+ onComplete: () => setProcessing(false)
749
+ });
564
750
  } catch (cause) {
565
751
  const error = cause instanceof Error ? cause : new Error("Checkout failed");
566
752
  setCheckoutError(error.message);
@@ -576,6 +762,7 @@ function PricingTable({
576
762
  background: C.bg
577
763
  }, children: [
578
764
  showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
765
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
579
766
  checkoutError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: checkoutError }),
580
767
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
581
768
  display: "grid",
@@ -929,7 +1116,16 @@ function CheckoutInner({
929
1116
  }
930
1117
  }
931
1118
  ),
932
- error !== null && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "var(--nozle-destructive, #dc2626)", margin: "0.5rem 0 0" }, children: error.message }),
1119
+ error !== null && /* @__PURE__ */ jsxRuntime.jsx(
1120
+ "p",
1121
+ {
1122
+ style: {
1123
+ color: "var(--nozle-destructive, #dc2626)",
1124
+ margin: "0.5rem 0 0"
1125
+ },
1126
+ children: error.message
1127
+ }
1128
+ ),
933
1129
  children ?? /* @__PURE__ */ jsxRuntime.jsx(
934
1130
  "button",
935
1131
  {
@@ -954,7 +1150,7 @@ function CheckoutInner({
954
1150
  )
955
1151
  ] }) });
956
1152
  }
957
- function Checkout({
1153
+ function StripeCheckout({
958
1154
  clientSecret,
959
1155
  publishableKey,
960
1156
  stripeAccount,
@@ -978,7 +1174,14 @@ function Checkout({
978
1174
  [clientSecret, onComplete]
979
1175
  );
980
1176
  if (clientSecret.startsWith("cs_")) {
981
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckoutProvider, { stripe: stripePromise, options: embeddedOptions, children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckout, {}) }) });
1177
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: /* @__PURE__ */ jsxRuntime.jsx(
1178
+ reactStripeJs.EmbeddedCheckoutProvider,
1179
+ {
1180
+ stripe: stripePromise,
1181
+ options: embeddedOptions,
1182
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.EmbeddedCheckout, {})
1183
+ }
1184
+ ) });
982
1185
  }
983
1186
  const options = { clientSecret, appearance };
984
1187
  return /* @__PURE__ */ jsxRuntime.jsx(reactStripeJs.Elements, { stripe: stripePromise, options, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -995,6 +1198,65 @@ function Checkout({
995
1198
  }
996
1199
  ) }, clientSecret);
997
1200
  }
1201
+ function ProviderCheckout(props) {
1202
+ const billing = useOptionalBillingContext();
1203
+ const [busy, setBusy] = React.useState(false);
1204
+ const [error, setError] = React.useState(null);
1205
+ const [processing, setProcessing] = React.useState(false);
1206
+ const pay = async () => {
1207
+ if (busy) return;
1208
+ setBusy(true);
1209
+ setError(null);
1210
+ try {
1211
+ await handleCheckoutResult(props.checkout, {
1212
+ verifyCheckout: billing?.verifyCheckout,
1213
+ getCheckoutStatus: billing?.getCheckoutStatus,
1214
+ onSuccess: (id) => {
1215
+ setProcessing(false);
1216
+ props.onSuccess?.(id);
1217
+ props.onComplete?.();
1218
+ },
1219
+ onComplete: () => {
1220
+ setProcessing(false);
1221
+ props.onComplete?.();
1222
+ },
1223
+ onProcessing: (status) => {
1224
+ setProcessing(true);
1225
+ props.onProcessing?.(status);
1226
+ }
1227
+ });
1228
+ } catch (cause) {
1229
+ const failure = cause instanceof Error ? cause : new Error("Checkout failed");
1230
+ setError(failure);
1231
+ props.onError?.(failure);
1232
+ } finally {
1233
+ setBusy(false);
1234
+ }
1235
+ };
1236
+ return /* @__PURE__ */ jsxRuntime.jsx(
1237
+ CheckoutContext.Provider,
1238
+ {
1239
+ value: { confirmPayment: pay, isProcessing: busy, error },
1240
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: props.className, children: [
1241
+ /* @__PURE__ */ jsxRuntime.jsx(
1242
+ "button",
1243
+ {
1244
+ type: "button",
1245
+ disabled: busy,
1246
+ "aria-busy": busy,
1247
+ onClick: () => void pay(),
1248
+ children: busy ? "Processing\u2026" : props.submitLabel || "Pay now"
1249
+ }
1250
+ ),
1251
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1252
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error.message })
1253
+ ] })
1254
+ }
1255
+ );
1256
+ }
1257
+ function Checkout(props) {
1258
+ return "checkout" in props ? /* @__PURE__ */ jsxRuntime.jsx(ProviderCheckout, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(StripeCheckout, { ...props });
1259
+ }
998
1260
  function UpgradeModal({
999
1261
  isOpen,
1000
1262
  planCode,
@@ -1008,7 +1270,8 @@ function UpgradeModal({
1008
1270
  onCancel,
1009
1271
  onError
1010
1272
  }) {
1011
- const { createCheckout } = useBillingContext();
1273
+ const { createCheckout, verifyCheckout, getCheckoutStatus } = useBillingContext();
1274
+ const [processing, setProcessing] = React.useState(false);
1012
1275
  const [error, setError] = React.useState(null);
1013
1276
  const [confirming, setConfirming] = React.useState(false);
1014
1277
  async function handleConfirm() {
@@ -1023,11 +1286,21 @@ function UpgradeModal({
1023
1286
  returnUrl: returnUrl ?? window.location.href
1024
1287
  });
1025
1288
  await handleCheckoutResult(result, {
1289
+ verifyCheckout,
1290
+ getCheckoutStatus,
1291
+ onSuccess: () => {
1292
+ setProcessing(false);
1293
+ onCompleted?.();
1294
+ },
1295
+ onProcessing: () => setProcessing(true),
1026
1296
  onStripeClientSecret: (clientSecret) => {
1027
1297
  onStripeClientSecret?.(clientSecret);
1028
1298
  onCheckoutStarted?.();
1029
1299
  },
1030
- onComplete: () => onCompleted?.(),
1300
+ onComplete: () => {
1301
+ setProcessing(false);
1302
+ onCompleted?.();
1303
+ },
1031
1304
  onScheduled: () => onScheduled?.()
1032
1305
  });
1033
1306
  onConfirm?.();
@@ -1070,6 +1343,7 @@ function UpgradeModal({
1070
1343
  },
1071
1344
  children: [
1072
1345
  /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 1rem" }, children: "Confirm plan change" }),
1346
+ processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
1073
1347
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", style: { color: "oklch(0.6 0.2 25)" }, children: error }),
1074
1348
  preview && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.5rem" }, children: [
1075
1349
  /* @__PURE__ */ jsxRuntime.jsxs(