@digilogiclabs/saas-factory-payments 0.1.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.
@@ -0,0 +1,1604 @@
1
+ // src/web/components/CheckoutButton.tsx
2
+ import { useState } from "react";
3
+ import { useStripe } from "@stripe/react-stripe-js";
4
+
5
+ // src/shared/providers/PaymentsProvider.tsx
6
+ import { createContext, useContext } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var PaymentsContext = createContext(
9
+ void 0
10
+ );
11
+ var usePaymentsConfig = () => {
12
+ const context = useContext(PaymentsContext);
13
+ if (!context) {
14
+ throw new Error("usePaymentsConfig must be used within PaymentsProvider");
15
+ }
16
+ return context;
17
+ };
18
+
19
+ // src/shared/constants.ts
20
+ var CURRENCY_SYMBOLS = {
21
+ USD: "$",
22
+ EUR: "\u20AC",
23
+ GBP: "\xA3",
24
+ JPY: "\xA5",
25
+ CAD: "C$",
26
+ AUD: "A$",
27
+ CHF: "CHF",
28
+ CNY: "\xA5",
29
+ SEK: "kr",
30
+ NZD: "NZ$"
31
+ };
32
+ var DEFAULT_CURRENCY = "USD";
33
+ var ERROR_MESSAGES = {
34
+ PROVIDER_NOT_CONFIGURED: "Payments provider is not properly configured",
35
+ STRIPE_NOT_LOADED: "Stripe has not been loaded yet",
36
+ INVALID_PRICE_ID: "Invalid price ID provided",
37
+ INVALID_CUSTOMER_ID: "Invalid customer ID provided",
38
+ CHECKOUT_FAILED: "Checkout session creation failed",
39
+ PAYMENT_FAILED: "Payment processing failed",
40
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
41
+ CUSTOMER_NOT_FOUND: "Customer not found"
42
+ };
43
+
44
+ // src/shared/utils/validation.ts
45
+ var validateEmail = (email) => {
46
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
47
+ return emailRegex.test(email);
48
+ };
49
+ var validateStripeId = (id, type) => {
50
+ const prefixes = {
51
+ customer: "cus_",
52
+ subscription: "sub_",
53
+ price: "price_",
54
+ product: "prod_",
55
+ payment_intent: "pi_"
56
+ };
57
+ return id.startsWith(prefixes[type]);
58
+ };
59
+
60
+ // src/web/components/CheckoutButton.tsx
61
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
62
+ var CheckoutButton = ({
63
+ priceId,
64
+ customerId,
65
+ customerEmail,
66
+ successUrl = typeof window !== "undefined" ? `${window.location.origin}/success` : "/success",
67
+ cancelUrl = typeof window !== "undefined" ? `${window.location.origin}/cancel` : "/cancel",
68
+ children,
69
+ className = "",
70
+ disabled = false,
71
+ allowPromotionCodes = true,
72
+ billingAddressCollection = "auto",
73
+ metadata,
74
+ trialPeriodDays,
75
+ onSuccess,
76
+ onError,
77
+ onLoading
78
+ }) => {
79
+ const stripe = useStripe();
80
+ const { isConfigured } = usePaymentsConfig();
81
+ const [loading, setLoading] = useState(false);
82
+ const handleCheckout = async () => {
83
+ if (!stripe) {
84
+ const error = ERROR_MESSAGES.STRIPE_NOT_LOADED;
85
+ onError?.(error);
86
+ console.error(error);
87
+ return;
88
+ }
89
+ if (!isConfigured) {
90
+ const error = ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED;
91
+ onError?.(error);
92
+ console.error(error);
93
+ return;
94
+ }
95
+ if (!validateStripeId(priceId, "price")) {
96
+ const error = ERROR_MESSAGES.INVALID_PRICE_ID;
97
+ onError?.(error);
98
+ console.error(error);
99
+ return;
100
+ }
101
+ if (customerId && !validateStripeId(customerId, "customer")) {
102
+ const error = ERROR_MESSAGES.INVALID_CUSTOMER_ID;
103
+ onError?.(error);
104
+ console.error(error);
105
+ return;
106
+ }
107
+ setLoading(true);
108
+ onLoading?.(true);
109
+ try {
110
+ const response = await fetch("/api/payments/create-checkout-session", {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json"
114
+ },
115
+ body: JSON.stringify({
116
+ priceId,
117
+ customerId,
118
+ customerEmail,
119
+ successUrl,
120
+ cancelUrl,
121
+ allowPromotionCodes,
122
+ billingAddressCollection,
123
+ metadata,
124
+ trialPeriodDays
125
+ })
126
+ });
127
+ if (!response.ok) {
128
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
129
+ throw new Error(errorData.error || ERROR_MESSAGES.CHECKOUT_FAILED);
130
+ }
131
+ const { sessionId } = await response.json();
132
+ if (!sessionId) {
133
+ throw new Error("No session ID returned from server");
134
+ }
135
+ const result = await stripe.redirectToCheckout({ sessionId });
136
+ if (result.error) {
137
+ throw new Error(result.error.message || ERROR_MESSAGES.CHECKOUT_FAILED);
138
+ }
139
+ onSuccess?.();
140
+ } catch (error) {
141
+ const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.CHECKOUT_FAILED;
142
+ onError?.(errorMessage);
143
+ console.error("Checkout error:", error);
144
+ } finally {
145
+ setLoading(false);
146
+ onLoading?.(false);
147
+ }
148
+ };
149
+ const isDisabled = !stripe || loading || disabled || !isConfigured;
150
+ return /* @__PURE__ */ jsx2(
151
+ "button",
152
+ {
153
+ onClick: handleCheckout,
154
+ disabled: isDisabled,
155
+ className: `
156
+ inline-flex items-center justify-center px-4 py-2
157
+ border border-transparent text-sm font-medium rounded-md
158
+ text-white bg-blue-600 hover:bg-blue-700
159
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
160
+ disabled:opacity-50 disabled:cursor-not-allowed
161
+ transition-colors duration-200
162
+ ${className}
163
+ `,
164
+ "aria-label": typeof children === "string" ? children : "Checkout",
165
+ children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
166
+ /* @__PURE__ */ jsxs(
167
+ "svg",
168
+ {
169
+ className: "animate-spin -ml-1 mr-3 h-4 w-4 text-white",
170
+ xmlns: "http://www.w3.org/2000/svg",
171
+ fill: "none",
172
+ viewBox: "0 0 24 24",
173
+ children: [
174
+ /* @__PURE__ */ jsx2(
175
+ "circle",
176
+ {
177
+ className: "opacity-25",
178
+ cx: "12",
179
+ cy: "12",
180
+ r: "10",
181
+ stroke: "currentColor",
182
+ strokeWidth: "4"
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsx2(
186
+ "path",
187
+ {
188
+ className: "opacity-75",
189
+ fill: "currentColor",
190
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
191
+ }
192
+ )
193
+ ]
194
+ }
195
+ ),
196
+ "Processing..."
197
+ ] }) : children
198
+ }
199
+ );
200
+ };
201
+
202
+ // src/shared/utils/formatting.ts
203
+ var formatCurrency = (amount, currency = DEFAULT_CURRENCY, options = {}) => {
204
+ const { showSymbol = true, showCents = true, locale = "en-US" } = options;
205
+ const currencyCode = currency.toUpperCase();
206
+ const isZeroDecimalCurrency = ["JPY", "KRW", "VND", "CLP"].includes(
207
+ currencyCode
208
+ );
209
+ const displayAmount = isZeroDecimalCurrency ? amount : amount / 100;
210
+ if (showSymbol) {
211
+ try {
212
+ return new Intl.NumberFormat(locale, {
213
+ style: "currency",
214
+ currency: currencyCode,
215
+ minimumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0,
216
+ maximumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0
217
+ }).format(displayAmount);
218
+ } catch (error) {
219
+ const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
220
+ const formattedAmount = showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
221
+ return `${symbol}${formattedAmount}`;
222
+ }
223
+ }
224
+ return showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
225
+ };
226
+ var formatPricingPlan = (plan) => {
227
+ const price = formatCurrency(plan.price, plan.currency);
228
+ const interval = plan.intervalCount && plan.intervalCount > 1 ? `${plan.intervalCount} ${plan.interval}s` : plan.interval;
229
+ return `${price}/${interval}`;
230
+ };
231
+ var formatTrialPeriod = (days) => {
232
+ if (days === 0) return "No trial";
233
+ if (days === 1) return "1 day trial";
234
+ if (days < 7) return `${days} days trial`;
235
+ if (days === 7) return "1 week trial";
236
+ if (days < 30) return `${Math.floor(days / 7)} weeks trial`;
237
+ if (days === 30) return "1 month trial";
238
+ return `${Math.floor(days / 30)} months trial`;
239
+ };
240
+
241
+ // src/web/components/PricingTable.tsx
242
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
243
+ var PricingTable = ({
244
+ plans,
245
+ customerId,
246
+ customerEmail,
247
+ successUrl,
248
+ cancelUrl,
249
+ className = "",
250
+ onPlanSelect,
251
+ onCheckoutSuccess,
252
+ onCheckoutError,
253
+ showFeatures = true,
254
+ showTrialInfo = true,
255
+ layout = "grid",
256
+ maxColumns = 3
257
+ }) => {
258
+ if (!plans || plans.length === 0) {
259
+ return /* @__PURE__ */ jsx3("div", { className: "text-center py-8 text-gray-500", children: "No pricing plans available" });
260
+ }
261
+ const gridCols = Math.min(plans.length, maxColumns);
262
+ const gridClass = layout === "grid" ? `grid gap-6 ${gridCols === 1 ? "grid-cols-1" : gridCols === 2 ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"}` : "space-y-6";
263
+ return /* @__PURE__ */ jsx3("div", { className: `pricing-table ${className}`, children: /* @__PURE__ */ jsx3("div", { className: gridClass, children: plans.map((plan) => /* @__PURE__ */ jsx3(
264
+ PricingCard,
265
+ {
266
+ plan,
267
+ customerId,
268
+ customerEmail,
269
+ successUrl,
270
+ cancelUrl,
271
+ onSelect: () => onPlanSelect?.(plan),
272
+ onCheckoutSuccess: () => onCheckoutSuccess?.(plan),
273
+ onCheckoutError: (error) => onCheckoutError?.(plan, error),
274
+ showFeatures,
275
+ showTrialInfo
276
+ },
277
+ plan.id
278
+ )) }) });
279
+ };
280
+ var PricingCard = ({
281
+ plan,
282
+ customerId,
283
+ customerEmail,
284
+ successUrl,
285
+ cancelUrl,
286
+ onSelect,
287
+ onCheckoutSuccess,
288
+ onCheckoutError,
289
+ showFeatures = true,
290
+ showTrialInfo = true
291
+ }) => {
292
+ const isPopular = plan.popular;
293
+ return /* @__PURE__ */ jsxs2(
294
+ "div",
295
+ {
296
+ className: `
297
+ relative bg-white rounded-lg shadow-lg overflow-hidden
298
+ ${isPopular ? "ring-2 ring-blue-500 ring-opacity-50" : "border border-gray-200"}
299
+ transition-transform duration-200 hover:scale-105
300
+ `,
301
+ children: [
302
+ isPopular && /* @__PURE__ */ jsx3("div", { className: "absolute top-0 left-0 right-0 bg-blue-500 text-white text-center py-2 text-sm font-medium", children: "Most Popular" }),
303
+ /* @__PURE__ */ jsxs2("div", { className: `p-6 ${isPopular ? "pt-12" : ""}`, children: [
304
+ /* @__PURE__ */ jsxs2("div", { className: "text-center mb-6", children: [
305
+ /* @__PURE__ */ jsx3("h3", { className: "text-xl font-semibold text-gray-900 mb-2", children: plan.name }),
306
+ plan.description && /* @__PURE__ */ jsx3("p", { className: "text-gray-600 text-sm mb-4", children: plan.description }),
307
+ /* @__PURE__ */ jsx3("div", { className: "mb-4", children: /* @__PURE__ */ jsx3("span", { className: "text-3xl font-bold text-gray-900", children: formatPricingPlan(plan) }) }),
308
+ showTrialInfo && plan.trialPeriodDays && plan.trialPeriodDays > 0 && /* @__PURE__ */ jsx3("div", { className: "text-sm text-green-600 font-medium", children: formatTrialPeriod(plan.trialPeriodDays) })
309
+ ] }),
310
+ showFeatures && plan.features && plan.features.length > 0 && /* @__PURE__ */ jsx3("div", { className: "mb-6", children: /* @__PURE__ */ jsx3("ul", { className: "space-y-3", children: plan.features.map((feature, index) => /* @__PURE__ */ jsxs2("li", { className: "flex items-start", children: [
311
+ /* @__PURE__ */ jsx3(
312
+ "svg",
313
+ {
314
+ className: "flex-shrink-0 w-5 h-5 text-green-500 mt-0.5 mr-3",
315
+ fill: "none",
316
+ stroke: "currentColor",
317
+ viewBox: "0 0 24 24",
318
+ children: /* @__PURE__ */ jsx3(
319
+ "path",
320
+ {
321
+ strokeLinecap: "round",
322
+ strokeLinejoin: "round",
323
+ strokeWidth: 2,
324
+ d: "M5 13l4 4L19 7"
325
+ }
326
+ )
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsx3("span", { className: "text-gray-700 text-sm", children: feature })
330
+ ] }, index)) }) }),
331
+ /* @__PURE__ */ jsx3("div", { className: "mt-6", children: /* @__PURE__ */ jsx3(
332
+ CheckoutButton,
333
+ {
334
+ priceId: plan.stripePriceId,
335
+ customerId,
336
+ customerEmail,
337
+ successUrl,
338
+ cancelUrl,
339
+ trialPeriodDays: plan.trialPeriodDays,
340
+ metadata: plan.metadata,
341
+ onSuccess: onCheckoutSuccess,
342
+ onError: onCheckoutError,
343
+ className: `
344
+ w-full justify-center py-3 px-4 text-base font-medium
345
+ ${isPopular ? "bg-blue-600 hover:bg-blue-700 text-white" : "bg-gray-100 hover:bg-gray-200 text-gray-900 border border-gray-300"}
346
+ `,
347
+ children: "Get Started"
348
+ }
349
+ ) })
350
+ ] })
351
+ ]
352
+ }
353
+ );
354
+ };
355
+
356
+ // src/web/components/PaymentForm.tsx
357
+ import { useState as useState2 } from "react";
358
+ import {
359
+ useStripe as useStripe2,
360
+ useElements,
361
+ CardElement,
362
+ PaymentElement
363
+ } from "@stripe/react-stripe-js";
364
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
365
+ var PaymentForm = ({
366
+ clientSecret,
367
+ customerId,
368
+ customerEmail,
369
+ amount,
370
+ currency = "usd",
371
+ description,
372
+ metadata,
373
+ onSuccess,
374
+ onError,
375
+ onLoading,
376
+ className = "",
377
+ showBillingDetails = true,
378
+ elementType = "payment"
379
+ }) => {
380
+ const stripe = useStripe2();
381
+ const elements = useElements();
382
+ const { isConfigured } = usePaymentsConfig();
383
+ const [loading, setLoading] = useState2(false);
384
+ const [email, setEmail] = useState2(customerEmail || "");
385
+ const [billingDetails, setBillingDetails] = useState2({
386
+ name: "",
387
+ email: customerEmail || "",
388
+ phone: "",
389
+ address: {
390
+ line1: "",
391
+ line2: "",
392
+ city: "",
393
+ state: "",
394
+ postal_code: "",
395
+ country: "US"
396
+ }
397
+ });
398
+ const handleSubmit = async (event) => {
399
+ event.preventDefault();
400
+ if (!stripe || !elements) {
401
+ const error = ERROR_MESSAGES.STRIPE_NOT_LOADED;
402
+ onError?.(error);
403
+ return;
404
+ }
405
+ if (!isConfigured) {
406
+ const error = ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED;
407
+ onError?.(error);
408
+ return;
409
+ }
410
+ if (showBillingDetails && email && !validateEmail(email)) {
411
+ onError?.("Please enter a valid email address");
412
+ return;
413
+ }
414
+ setLoading(true);
415
+ onLoading?.(true);
416
+ try {
417
+ let result;
418
+ if (elementType === "payment" && clientSecret) {
419
+ result = await stripe.confirmPayment({
420
+ elements,
421
+ confirmParams: {
422
+ return_url: window.location.href,
423
+ receipt_email: email || billingDetails.email
424
+ },
425
+ redirect: "if_required"
426
+ });
427
+ } else if (elementType === "card") {
428
+ const cardElement = elements.getElement(CardElement);
429
+ if (!cardElement) {
430
+ throw new Error("Card element not found");
431
+ }
432
+ if (clientSecret) {
433
+ result = await stripe.confirmCardPayment(clientSecret, {
434
+ payment_method: {
435
+ card: cardElement,
436
+ billing_details: showBillingDetails ? {
437
+ name: billingDetails.name || void 0,
438
+ email: billingDetails.email || void 0,
439
+ phone: billingDetails.phone || void 0,
440
+ address: billingDetails.address.line1 ? billingDetails.address : void 0
441
+ } : void 0
442
+ }
443
+ });
444
+ } else if (amount) {
445
+ const response = await fetch("/api/payments/create-payment-intent", {
446
+ method: "POST",
447
+ headers: { "Content-Type": "application/json" },
448
+ body: JSON.stringify({
449
+ amount,
450
+ currency,
451
+ customerId,
452
+ description,
453
+ metadata
454
+ })
455
+ });
456
+ if (!response.ok) {
457
+ throw new Error("Failed to create payment intent");
458
+ }
459
+ const { client_secret } = await response.json();
460
+ result = await stripe.confirmCardPayment(client_secret, {
461
+ payment_method: {
462
+ card: cardElement,
463
+ billing_details: showBillingDetails ? {
464
+ name: billingDetails.name || void 0,
465
+ email: billingDetails.email || void 0,
466
+ phone: billingDetails.phone || void 0,
467
+ address: billingDetails.address.line1 ? billingDetails.address : void 0
468
+ } : void 0
469
+ }
470
+ });
471
+ } else {
472
+ throw new Error("Either clientSecret or amount is required");
473
+ }
474
+ }
475
+ if (result?.error) {
476
+ throw new Error(result.error.message || ERROR_MESSAGES.PAYMENT_FAILED);
477
+ }
478
+ if (result?.paymentIntent?.status === "succeeded") {
479
+ onSuccess?.(result.paymentIntent);
480
+ }
481
+ } catch (error) {
482
+ const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.PAYMENT_FAILED;
483
+ onError?.(errorMessage);
484
+ console.error("Payment error:", error);
485
+ } finally {
486
+ setLoading(false);
487
+ onLoading?.(false);
488
+ }
489
+ };
490
+ const cardElementOptions = {
491
+ style: {
492
+ base: {
493
+ fontSize: "16px",
494
+ color: "#424770",
495
+ "::placeholder": {
496
+ color: "#aab7c4"
497
+ }
498
+ },
499
+ invalid: {
500
+ color: "#9e2146"
501
+ }
502
+ }
503
+ };
504
+ return /* @__PURE__ */ jsxs3("form", { onSubmit: handleSubmit, className: `payment-form ${className}`, children: [
505
+ showBillingDetails && /* @__PURE__ */ jsxs3("div", { className: "mb-6 space-y-4", children: [
506
+ /* @__PURE__ */ jsxs3("div", { children: [
507
+ /* @__PURE__ */ jsx4(
508
+ "label",
509
+ {
510
+ htmlFor: "email",
511
+ className: "block text-sm font-medium text-gray-700 mb-1",
512
+ children: "Email"
513
+ }
514
+ ),
515
+ /* @__PURE__ */ jsx4(
516
+ "input",
517
+ {
518
+ type: "email",
519
+ id: "email",
520
+ value: email,
521
+ onChange: (e) => {
522
+ setEmail(e.target.value);
523
+ setBillingDetails((prev) => ({ ...prev, email: e.target.value }));
524
+ },
525
+ className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
526
+ placeholder: "your@email.com",
527
+ required: true
528
+ }
529
+ )
530
+ ] }),
531
+ /* @__PURE__ */ jsxs3("div", { children: [
532
+ /* @__PURE__ */ jsx4(
533
+ "label",
534
+ {
535
+ htmlFor: "name",
536
+ className: "block text-sm font-medium text-gray-700 mb-1",
537
+ children: "Full Name"
538
+ }
539
+ ),
540
+ /* @__PURE__ */ jsx4(
541
+ "input",
542
+ {
543
+ type: "text",
544
+ id: "name",
545
+ value: billingDetails.name,
546
+ onChange: (e) => setBillingDetails((prev) => ({ ...prev, name: e.target.value })),
547
+ className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",
548
+ placeholder: "John Doe"
549
+ }
550
+ )
551
+ ] })
552
+ ] }),
553
+ /* @__PURE__ */ jsxs3("div", { className: "mb-6", children: [
554
+ /* @__PURE__ */ jsx4("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Payment Information" }),
555
+ /* @__PURE__ */ jsx4("div", { className: "p-3 border border-gray-300 rounded-md", children: elementType === "payment" ? /* @__PURE__ */ jsx4(PaymentElement, {}) : /* @__PURE__ */ jsx4(CardElement, { options: cardElementOptions }) })
556
+ ] }),
557
+ /* @__PURE__ */ jsx4(
558
+ "button",
559
+ {
560
+ type: "submit",
561
+ disabled: !stripe || loading,
562
+ className: "w-full bg-blue-600 text-white py-3 px-4 rounded-md font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200",
563
+ children: loading ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
564
+ /* @__PURE__ */ jsxs3(
565
+ "svg",
566
+ {
567
+ className: "animate-spin -ml-1 mr-3 h-4 w-4 text-white inline",
568
+ xmlns: "http://www.w3.org/2000/svg",
569
+ fill: "none",
570
+ viewBox: "0 0 24 24",
571
+ children: [
572
+ /* @__PURE__ */ jsx4(
573
+ "circle",
574
+ {
575
+ className: "opacity-25",
576
+ cx: "12",
577
+ cy: "12",
578
+ r: "10",
579
+ stroke: "currentColor",
580
+ strokeWidth: "4"
581
+ }
582
+ ),
583
+ /* @__PURE__ */ jsx4(
584
+ "path",
585
+ {
586
+ className: "opacity-75",
587
+ fill: "currentColor",
588
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
589
+ }
590
+ )
591
+ ]
592
+ }
593
+ ),
594
+ "Processing..."
595
+ ] }) : `Pay ${amount ? `$${(amount / 100).toFixed(2)}` : ""}`
596
+ }
597
+ )
598
+ ] });
599
+ };
600
+
601
+ // src/web/components/BillingPortal.tsx
602
+ import { useState as useState3 } from "react";
603
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
604
+ var BillingPortal = ({
605
+ customerId,
606
+ returnUrl = typeof window !== "undefined" ? window.location.href : "/",
607
+ children,
608
+ className = "",
609
+ onSuccess,
610
+ onError,
611
+ onLoading
612
+ }) => {
613
+ const { isConfigured } = usePaymentsConfig();
614
+ const [loading, setLoading] = useState3(false);
615
+ const handleOpenPortal = async () => {
616
+ if (!isConfigured) {
617
+ const error = ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED;
618
+ onError?.(error);
619
+ console.error(error);
620
+ return;
621
+ }
622
+ if (!validateStripeId(customerId, "customer")) {
623
+ const error = ERROR_MESSAGES.INVALID_CUSTOMER_ID;
624
+ onError?.(error);
625
+ console.error(error);
626
+ return;
627
+ }
628
+ setLoading(true);
629
+ onLoading?.(true);
630
+ try {
631
+ const response = await fetch("/api/payments/create-portal-session", {
632
+ method: "POST",
633
+ headers: {
634
+ "Content-Type": "application/json"
635
+ },
636
+ body: JSON.stringify({
637
+ customerId,
638
+ returnUrl
639
+ })
640
+ });
641
+ if (!response.ok) {
642
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
643
+ throw new Error(errorData.error || "Failed to create portal session");
644
+ }
645
+ const { url } = await response.json();
646
+ if (!url) {
647
+ throw new Error("No portal URL returned from server");
648
+ }
649
+ window.location.href = url;
650
+ onSuccess?.();
651
+ } catch (error) {
652
+ const errorMessage = error instanceof Error ? error.message : "Failed to open billing portal";
653
+ onError?.(errorMessage);
654
+ console.error("Billing portal error:", error);
655
+ } finally {
656
+ setLoading(false);
657
+ onLoading?.(false);
658
+ }
659
+ };
660
+ const isDisabled = loading || !isConfigured || !customerId;
661
+ return /* @__PURE__ */ jsx5(
662
+ "button",
663
+ {
664
+ onClick: handleOpenPortal,
665
+ disabled: isDisabled,
666
+ className: `
667
+ inline-flex items-center justify-center px-4 py-2
668
+ border border-gray-300 text-sm font-medium rounded-md
669
+ text-gray-700 bg-white hover:bg-gray-50
670
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
671
+ disabled:opacity-50 disabled:cursor-not-allowed
672
+ transition-colors duration-200
673
+ ${className}
674
+ `,
675
+ "aria-label": "Manage billing",
676
+ children: loading ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
677
+ /* @__PURE__ */ jsxs4(
678
+ "svg",
679
+ {
680
+ className: "animate-spin -ml-1 mr-3 h-4 w-4 text-gray-700",
681
+ xmlns: "http://www.w3.org/2000/svg",
682
+ fill: "none",
683
+ viewBox: "0 0 24 24",
684
+ children: [
685
+ /* @__PURE__ */ jsx5(
686
+ "circle",
687
+ {
688
+ className: "opacity-25",
689
+ cx: "12",
690
+ cy: "12",
691
+ r: "10",
692
+ stroke: "currentColor",
693
+ strokeWidth: "4"
694
+ }
695
+ ),
696
+ /* @__PURE__ */ jsx5(
697
+ "path",
698
+ {
699
+ className: "opacity-75",
700
+ fill: "currentColor",
701
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
702
+ }
703
+ )
704
+ ]
705
+ }
706
+ ),
707
+ "Loading..."
708
+ ] }) : children || /* @__PURE__ */ jsxs4(Fragment3, { children: [
709
+ /* @__PURE__ */ jsxs4(
710
+ "svg",
711
+ {
712
+ className: "w-4 h-4 mr-2",
713
+ fill: "none",
714
+ stroke: "currentColor",
715
+ viewBox: "0 0 24 24",
716
+ children: [
717
+ /* @__PURE__ */ jsx5(
718
+ "path",
719
+ {
720
+ strokeLinecap: "round",
721
+ strokeLinejoin: "round",
722
+ strokeWidth: 2,
723
+ d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
724
+ }
725
+ ),
726
+ /* @__PURE__ */ jsx5(
727
+ "path",
728
+ {
729
+ strokeLinecap: "round",
730
+ strokeLinejoin: "round",
731
+ strokeWidth: 2,
732
+ d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
733
+ }
734
+ )
735
+ ]
736
+ }
737
+ ),
738
+ "Manage Billing"
739
+ ] })
740
+ }
741
+ );
742
+ };
743
+
744
+ // src/web/providers/StripeProvider.tsx
745
+ import React5 from "react";
746
+ import { loadStripe } from "@stripe/stripe-js";
747
+ import { Elements } from "@stripe/react-stripe-js";
748
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
749
+ var stripePromise = null;
750
+ var getStripe = (publishableKey) => {
751
+ if (!stripePromise) {
752
+ stripePromise = loadStripe(publishableKey);
753
+ }
754
+ return stripePromise;
755
+ };
756
+ var StripeProvider = ({
757
+ children,
758
+ options = {}
759
+ }) => {
760
+ const { config, isConfigured } = usePaymentsConfig();
761
+ const stripe = React5.useMemo(() => {
762
+ if (!isConfigured) {
763
+ console.error(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
764
+ return null;
765
+ }
766
+ return getStripe(config.publishableKey);
767
+ }, [config.publishableKey, isConfigured]);
768
+ if (!isConfigured) {
769
+ if (config.environment === "development") {
770
+ return /* @__PURE__ */ jsxs5(
771
+ "div",
772
+ {
773
+ style: {
774
+ padding: "20px",
775
+ backgroundColor: "#fee",
776
+ border: "1px solid #fcc",
777
+ borderRadius: "4px",
778
+ margin: "10px",
779
+ fontFamily: "monospace"
780
+ },
781
+ children: [
782
+ /* @__PURE__ */ jsx6("strong", { children: "Payments Provider Error:" }),
783
+ /* @__PURE__ */ jsx6("br", {}),
784
+ ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED,
785
+ /* @__PURE__ */ jsx6("br", {}),
786
+ "Please check your PaymentsProvider configuration."
787
+ ]
788
+ }
789
+ );
790
+ }
791
+ return null;
792
+ }
793
+ const elementsOptions = {
794
+ fonts: options.fonts,
795
+ locale: options.locale,
796
+ appearance: {
797
+ theme: "stripe"
798
+ }
799
+ };
800
+ return /* @__PURE__ */ jsx6(Elements, { stripe, options: elementsOptions, children });
801
+ };
802
+
803
+ // src/web/hooks/useSubscription.ts
804
+ import { useState as useState4, useEffect, useCallback } from "react";
805
+ var useSubscription = (options = {}) => {
806
+ const { customerId, subscriptionId, autoFetch = true, onError } = options;
807
+ const [subscription, setSubscription] = useState4(null);
808
+ const [subscriptions, setSubscriptions] = useState4([]);
809
+ const [loading, setLoading] = useState4(false);
810
+ const [error, setError] = useState4(null);
811
+ const handleError = useCallback(
812
+ (errorMessage) => {
813
+ setError(errorMessage);
814
+ onError?.(errorMessage);
815
+ console.error("Subscription error:", errorMessage);
816
+ },
817
+ [onError]
818
+ );
819
+ const fetchSubscription = useCallback(
820
+ async (id) => {
821
+ if (!validateStripeId(id, "subscription")) {
822
+ handleError("Invalid subscription ID");
823
+ return null;
824
+ }
825
+ try {
826
+ const response = await fetch(`/api/payments/subscription/${id}`);
827
+ if (!response.ok) {
828
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
829
+ throw new Error(
830
+ errorData.error || ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND
831
+ );
832
+ }
833
+ const data = await response.json();
834
+ return data;
835
+ } catch (err) {
836
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND;
837
+ handleError(errorMessage);
838
+ return null;
839
+ }
840
+ },
841
+ [handleError]
842
+ );
843
+ const fetchCustomerSubscriptions = useCallback(
844
+ async (id) => {
845
+ if (!validateStripeId(id, "customer")) {
846
+ handleError("Invalid customer ID");
847
+ return [];
848
+ }
849
+ try {
850
+ const response = await fetch(
851
+ `/api/payments/customer/${id}/subscriptions`
852
+ );
853
+ if (!response.ok) {
854
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
855
+ throw new Error(errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND);
856
+ }
857
+ const data = await response.json();
858
+ return data.subscriptions || [];
859
+ } catch (err) {
860
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND;
861
+ handleError(errorMessage);
862
+ return [];
863
+ }
864
+ },
865
+ [handleError]
866
+ );
867
+ const refetch = useCallback(async () => {
868
+ if (!customerId && !subscriptionId) {
869
+ handleError("Either customerId or subscriptionId is required");
870
+ return;
871
+ }
872
+ setLoading(true);
873
+ setError(null);
874
+ try {
875
+ if (subscriptionId) {
876
+ const sub = await fetchSubscription(subscriptionId);
877
+ setSubscription(sub);
878
+ }
879
+ if (customerId) {
880
+ const subs = await fetchCustomerSubscriptions(customerId);
881
+ setSubscriptions(subs);
882
+ if (!subscriptionId && subs.length > 0) {
883
+ const activeSub = subs.find((s) => s.status === "active") || subs[0];
884
+ setSubscription(activeSub);
885
+ }
886
+ }
887
+ } finally {
888
+ setLoading(false);
889
+ }
890
+ }, [
891
+ customerId,
892
+ subscriptionId,
893
+ fetchSubscription,
894
+ fetchCustomerSubscriptions,
895
+ handleError
896
+ ]);
897
+ const cancel = useCallback(
898
+ async (targetSubscriptionId) => {
899
+ const idToCancel = targetSubscriptionId || subscriptionId;
900
+ if (!idToCancel) {
901
+ handleError("Subscription ID is required to cancel");
902
+ return false;
903
+ }
904
+ if (!validateStripeId(idToCancel, "subscription")) {
905
+ handleError("Invalid subscription ID");
906
+ return false;
907
+ }
908
+ try {
909
+ const response = await fetch(
910
+ `/api/payments/subscription/${idToCancel}/cancel`,
911
+ {
912
+ method: "POST",
913
+ headers: { "Content-Type": "application/json" }
914
+ }
915
+ );
916
+ if (!response.ok) {
917
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
918
+ throw new Error(errorData.error || "Failed to cancel subscription");
919
+ }
920
+ await refetch();
921
+ return true;
922
+ } catch (err) {
923
+ const errorMessage = err instanceof Error ? err.message : "Failed to cancel subscription";
924
+ handleError(errorMessage);
925
+ return false;
926
+ }
927
+ },
928
+ [subscriptionId, refetch, handleError]
929
+ );
930
+ const reactivate = useCallback(
931
+ async (targetSubscriptionId) => {
932
+ const idToReactivate = targetSubscriptionId || subscriptionId;
933
+ if (!idToReactivate) {
934
+ handleError("Subscription ID is required to reactivate");
935
+ return false;
936
+ }
937
+ if (!validateStripeId(idToReactivate, "subscription")) {
938
+ handleError("Invalid subscription ID");
939
+ return false;
940
+ }
941
+ try {
942
+ const response = await fetch(
943
+ `/api/payments/subscription/${idToReactivate}/reactivate`,
944
+ {
945
+ method: "POST",
946
+ headers: { "Content-Type": "application/json" }
947
+ }
948
+ );
949
+ if (!response.ok) {
950
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
951
+ throw new Error(
952
+ errorData.error || "Failed to reactivate subscription"
953
+ );
954
+ }
955
+ await refetch();
956
+ return true;
957
+ } catch (err) {
958
+ const errorMessage = err instanceof Error ? err.message : "Failed to reactivate subscription";
959
+ handleError(errorMessage);
960
+ return false;
961
+ }
962
+ },
963
+ [subscriptionId, refetch, handleError]
964
+ );
965
+ const updatePaymentMethod = useCallback(
966
+ async (targetSubscriptionId, paymentMethodId) => {
967
+ if (!validateStripeId(targetSubscriptionId, "subscription")) {
968
+ handleError("Invalid subscription ID");
969
+ return false;
970
+ }
971
+ try {
972
+ const response = await fetch(
973
+ `/api/payments/subscription/${targetSubscriptionId}/payment-method`,
974
+ {
975
+ method: "PUT",
976
+ headers: { "Content-Type": "application/json" },
977
+ body: JSON.stringify({ paymentMethodId })
978
+ }
979
+ );
980
+ if (!response.ok) {
981
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
982
+ throw new Error(errorData.error || "Failed to update payment method");
983
+ }
984
+ await refetch();
985
+ return true;
986
+ } catch (err) {
987
+ const errorMessage = err instanceof Error ? err.message : "Failed to update payment method";
988
+ handleError(errorMessage);
989
+ return false;
990
+ }
991
+ },
992
+ [refetch, handleError]
993
+ );
994
+ useEffect(() => {
995
+ if (autoFetch && (customerId || subscriptionId)) {
996
+ refetch();
997
+ }
998
+ }, [autoFetch, customerId, subscriptionId, refetch]);
999
+ return {
1000
+ subscription,
1001
+ subscriptions,
1002
+ loading,
1003
+ error,
1004
+ refetch,
1005
+ cancel,
1006
+ reactivate,
1007
+ updatePaymentMethod
1008
+ };
1009
+ };
1010
+
1011
+ // src/web/hooks/useCheckout.ts
1012
+ import { useState as useState5, useCallback as useCallback2 } from "react";
1013
+ import { useStripe as useStripe3 } from "@stripe/react-stripe-js";
1014
+ var useCheckout = (options = {}) => {
1015
+ const { onSuccess, onError, onLoading } = options;
1016
+ const stripe = useStripe3();
1017
+ const { isConfigured } = usePaymentsConfig();
1018
+ const [loading, setLoading] = useState5(false);
1019
+ const [error, setError] = useState5(null);
1020
+ const handleError = useCallback2(
1021
+ (errorMessage) => {
1022
+ setError(errorMessage);
1023
+ onError?.(errorMessage);
1024
+ console.error("Checkout error:", errorMessage);
1025
+ },
1026
+ [onError]
1027
+ );
1028
+ const validateCheckoutParams = useCallback2(
1029
+ (params) => {
1030
+ if (!params.priceId) {
1031
+ return "Price ID is required";
1032
+ }
1033
+ if (!validateStripeId(params.priceId, "price")) {
1034
+ return ERROR_MESSAGES.INVALID_PRICE_ID;
1035
+ }
1036
+ if (params.customerId && !validateStripeId(params.customerId, "customer")) {
1037
+ return ERROR_MESSAGES.INVALID_CUSTOMER_ID;
1038
+ }
1039
+ if (params.customerEmail && !validateEmail(params.customerEmail)) {
1040
+ return "Invalid customer email";
1041
+ }
1042
+ if (!params.successUrl) {
1043
+ return "Success URL is required";
1044
+ }
1045
+ if (!params.cancelUrl) {
1046
+ return "Cancel URL is required";
1047
+ }
1048
+ return null;
1049
+ },
1050
+ []
1051
+ );
1052
+ const createCheckoutSession2 = useCallback2(
1053
+ async (params) => {
1054
+ if (!isConfigured) {
1055
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1056
+ return null;
1057
+ }
1058
+ const validationError = validateCheckoutParams(params);
1059
+ if (validationError) {
1060
+ handleError(validationError);
1061
+ return null;
1062
+ }
1063
+ setLoading(true);
1064
+ setError(null);
1065
+ onLoading?.(true);
1066
+ try {
1067
+ const response = await fetch("/api/payments/create-checkout-session", {
1068
+ method: "POST",
1069
+ headers: {
1070
+ "Content-Type": "application/json"
1071
+ },
1072
+ body: JSON.stringify(params)
1073
+ });
1074
+ if (!response.ok) {
1075
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1076
+ throw new Error(errorData.error || ERROR_MESSAGES.CHECKOUT_FAILED);
1077
+ }
1078
+ const { sessionId } = await response.json();
1079
+ if (!sessionId) {
1080
+ throw new Error("No session ID returned from server");
1081
+ }
1082
+ onSuccess?.(sessionId);
1083
+ return sessionId;
1084
+ } catch (err) {
1085
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CHECKOUT_FAILED;
1086
+ handleError(errorMessage);
1087
+ return null;
1088
+ } finally {
1089
+ setLoading(false);
1090
+ onLoading?.(false);
1091
+ }
1092
+ },
1093
+ [isConfigured, validateCheckoutParams, handleError, onSuccess, onLoading]
1094
+ );
1095
+ const redirectToCheckout2 = useCallback2(
1096
+ async (params) => {
1097
+ if (!stripe) {
1098
+ handleError(ERROR_MESSAGES.STRIPE_NOT_LOADED);
1099
+ return false;
1100
+ }
1101
+ const sessionId = await createCheckoutSession2(params);
1102
+ if (!sessionId) {
1103
+ return false;
1104
+ }
1105
+ try {
1106
+ const result = await stripe.redirectToCheckout({ sessionId });
1107
+ if (result.error) {
1108
+ throw new Error(
1109
+ result.error.message || ERROR_MESSAGES.CHECKOUT_FAILED
1110
+ );
1111
+ }
1112
+ return true;
1113
+ } catch (err) {
1114
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CHECKOUT_FAILED;
1115
+ handleError(errorMessage);
1116
+ return false;
1117
+ }
1118
+ },
1119
+ [stripe, createCheckoutSession2, handleError]
1120
+ );
1121
+ const createPaymentIntent2 = useCallback2(
1122
+ async (params) => {
1123
+ if (!isConfigured) {
1124
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1125
+ return null;
1126
+ }
1127
+ if (params.amount <= 0) {
1128
+ handleError("Amount must be greater than 0");
1129
+ return null;
1130
+ }
1131
+ if (params.customerId && !validateStripeId(params.customerId, "customer")) {
1132
+ handleError(ERROR_MESSAGES.INVALID_CUSTOMER_ID);
1133
+ return null;
1134
+ }
1135
+ setLoading(true);
1136
+ setError(null);
1137
+ onLoading?.(true);
1138
+ try {
1139
+ const response = await fetch("/api/payments/create-payment-intent", {
1140
+ method: "POST",
1141
+ headers: {
1142
+ "Content-Type": "application/json"
1143
+ },
1144
+ body: JSON.stringify(params)
1145
+ });
1146
+ if (!response.ok) {
1147
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1148
+ throw new Error(errorData.error || "Failed to create payment intent");
1149
+ }
1150
+ const { client_secret } = await response.json();
1151
+ if (!client_secret) {
1152
+ throw new Error("No client secret returned from server");
1153
+ }
1154
+ return client_secret;
1155
+ } catch (err) {
1156
+ const errorMessage = err instanceof Error ? err.message : "Failed to create payment intent";
1157
+ handleError(errorMessage);
1158
+ return null;
1159
+ } finally {
1160
+ setLoading(false);
1161
+ onLoading?.(false);
1162
+ }
1163
+ },
1164
+ [isConfigured, handleError, onLoading]
1165
+ );
1166
+ return {
1167
+ loading,
1168
+ error,
1169
+ createCheckoutSession: createCheckoutSession2,
1170
+ redirectToCheckout: redirectToCheckout2,
1171
+ createPaymentIntent: createPaymentIntent2
1172
+ };
1173
+ };
1174
+
1175
+ // src/web/hooks/useCustomer.ts
1176
+ import { useState as useState6, useEffect as useEffect2, useCallback as useCallback3 } from "react";
1177
+ var useCustomer = (options = {}) => {
1178
+ const { customerId, autoFetch = true, onError } = options;
1179
+ const [customer, setCustomer] = useState6(null);
1180
+ const [paymentMethods, setPaymentMethods] = useState6([]);
1181
+ const [loading, setLoading] = useState6(false);
1182
+ const [error, setError] = useState6(null);
1183
+ const handleError = useCallback3(
1184
+ (errorMessage) => {
1185
+ setError(errorMessage);
1186
+ onError?.(errorMessage);
1187
+ console.error("Customer error:", errorMessage);
1188
+ },
1189
+ [onError]
1190
+ );
1191
+ const fetchCustomer = useCallback3(
1192
+ async (id) => {
1193
+ if (!validateStripeId(id, "customer")) {
1194
+ handleError("Invalid customer ID");
1195
+ return null;
1196
+ }
1197
+ try {
1198
+ const response = await fetch(`/api/payments/customer/${id}`);
1199
+ if (!response.ok) {
1200
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1201
+ throw new Error(errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND);
1202
+ }
1203
+ const data = await response.json();
1204
+ return data;
1205
+ } catch (err) {
1206
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND;
1207
+ handleError(errorMessage);
1208
+ return null;
1209
+ }
1210
+ },
1211
+ [handleError]
1212
+ );
1213
+ const refetch = useCallback3(async () => {
1214
+ if (!customerId) {
1215
+ handleError("Customer ID is required");
1216
+ return;
1217
+ }
1218
+ setLoading(true);
1219
+ setError(null);
1220
+ try {
1221
+ const customerData = await fetchCustomer(customerId);
1222
+ setCustomer(customerData);
1223
+ } finally {
1224
+ setLoading(false);
1225
+ }
1226
+ }, [customerId, fetchCustomer, handleError]);
1227
+ const createCustomer = useCallback3(
1228
+ async (params) => {
1229
+ if (!validateEmail(params.email)) {
1230
+ handleError("Invalid email address");
1231
+ return null;
1232
+ }
1233
+ setLoading(true);
1234
+ setError(null);
1235
+ try {
1236
+ const response = await fetch("/api/payments/customer", {
1237
+ method: "POST",
1238
+ headers: { "Content-Type": "application/json" },
1239
+ body: JSON.stringify(params)
1240
+ });
1241
+ if (!response.ok) {
1242
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1243
+ throw new Error(errorData.error || "Failed to create customer");
1244
+ }
1245
+ const customerData = await response.json();
1246
+ setCustomer(customerData);
1247
+ return customerData;
1248
+ } catch (err) {
1249
+ const errorMessage = err instanceof Error ? err.message : "Failed to create customer";
1250
+ handleError(errorMessage);
1251
+ return null;
1252
+ } finally {
1253
+ setLoading(false);
1254
+ }
1255
+ },
1256
+ [handleError]
1257
+ );
1258
+ const updateCustomer = useCallback3(
1259
+ async (params) => {
1260
+ if (!customerId) {
1261
+ handleError("Customer ID is required");
1262
+ return false;
1263
+ }
1264
+ if (params.email && !validateEmail(params.email)) {
1265
+ handleError("Invalid email address");
1266
+ return false;
1267
+ }
1268
+ setLoading(true);
1269
+ setError(null);
1270
+ try {
1271
+ const response = await fetch(`/api/payments/customer/${customerId}`, {
1272
+ method: "PUT",
1273
+ headers: { "Content-Type": "application/json" },
1274
+ body: JSON.stringify(params)
1275
+ });
1276
+ if (!response.ok) {
1277
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1278
+ throw new Error(errorData.error || "Failed to update customer");
1279
+ }
1280
+ await refetch();
1281
+ return true;
1282
+ } catch (err) {
1283
+ const errorMessage = err instanceof Error ? err.message : "Failed to update customer";
1284
+ handleError(errorMessage);
1285
+ return false;
1286
+ } finally {
1287
+ setLoading(false);
1288
+ }
1289
+ },
1290
+ [customerId, refetch, handleError]
1291
+ );
1292
+ const deleteCustomer = useCallback3(async () => {
1293
+ if (!customerId) {
1294
+ handleError("Customer ID is required");
1295
+ return false;
1296
+ }
1297
+ setLoading(true);
1298
+ setError(null);
1299
+ try {
1300
+ const response = await fetch(`/api/payments/customer/${customerId}`, {
1301
+ method: "DELETE"
1302
+ });
1303
+ if (!response.ok) {
1304
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1305
+ throw new Error(errorData.error || "Failed to delete customer");
1306
+ }
1307
+ setCustomer(null);
1308
+ setPaymentMethods([]);
1309
+ return true;
1310
+ } catch (err) {
1311
+ const errorMessage = err instanceof Error ? err.message : "Failed to delete customer";
1312
+ handleError(errorMessage);
1313
+ return false;
1314
+ } finally {
1315
+ setLoading(false);
1316
+ }
1317
+ }, [customerId, handleError]);
1318
+ const fetchPaymentMethods = useCallback3(async () => {
1319
+ if (!customerId) {
1320
+ handleError("Customer ID is required");
1321
+ return [];
1322
+ }
1323
+ try {
1324
+ const response = await fetch(
1325
+ `/api/payments/customer/${customerId}/payment-methods`
1326
+ );
1327
+ if (!response.ok) {
1328
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1329
+ throw new Error(errorData.error || "Failed to fetch payment methods");
1330
+ }
1331
+ const data = await response.json();
1332
+ const methods = data.paymentMethods || [];
1333
+ setPaymentMethods(methods);
1334
+ return methods;
1335
+ } catch (err) {
1336
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch payment methods";
1337
+ handleError(errorMessage);
1338
+ return [];
1339
+ }
1340
+ }, [customerId, handleError]);
1341
+ const addPaymentMethod = useCallback3(
1342
+ async (paymentMethodId) => {
1343
+ if (!customerId) {
1344
+ handleError("Customer ID is required");
1345
+ return false;
1346
+ }
1347
+ try {
1348
+ const response = await fetch(
1349
+ `/api/payments/customer/${customerId}/payment-methods`,
1350
+ {
1351
+ method: "POST",
1352
+ headers: { "Content-Type": "application/json" },
1353
+ body: JSON.stringify({ paymentMethodId })
1354
+ }
1355
+ );
1356
+ if (!response.ok) {
1357
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1358
+ throw new Error(errorData.error || "Failed to add payment method");
1359
+ }
1360
+ await fetchPaymentMethods();
1361
+ return true;
1362
+ } catch (err) {
1363
+ const errorMessage = err instanceof Error ? err.message : "Failed to add payment method";
1364
+ handleError(errorMessage);
1365
+ return false;
1366
+ }
1367
+ },
1368
+ [customerId, fetchPaymentMethods, handleError]
1369
+ );
1370
+ const removePaymentMethod = useCallback3(
1371
+ async (paymentMethodId) => {
1372
+ if (!customerId) {
1373
+ handleError("Customer ID is required");
1374
+ return false;
1375
+ }
1376
+ try {
1377
+ const response = await fetch(
1378
+ `/api/payments/customer/${customerId}/payment-methods/${paymentMethodId}`,
1379
+ {
1380
+ method: "DELETE"
1381
+ }
1382
+ );
1383
+ if (!response.ok) {
1384
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1385
+ throw new Error(errorData.error || "Failed to remove payment method");
1386
+ }
1387
+ await fetchPaymentMethods();
1388
+ return true;
1389
+ } catch (err) {
1390
+ const errorMessage = err instanceof Error ? err.message : "Failed to remove payment method";
1391
+ handleError(errorMessage);
1392
+ return false;
1393
+ }
1394
+ },
1395
+ [customerId, fetchPaymentMethods, handleError]
1396
+ );
1397
+ const setDefaultPaymentMethod = useCallback3(
1398
+ async (paymentMethodId) => {
1399
+ if (!customerId) {
1400
+ handleError("Customer ID is required");
1401
+ return false;
1402
+ }
1403
+ try {
1404
+ const response = await fetch(
1405
+ `/api/payments/customer/${customerId}/default-payment-method`,
1406
+ {
1407
+ method: "PUT",
1408
+ headers: { "Content-Type": "application/json" },
1409
+ body: JSON.stringify({ paymentMethodId })
1410
+ }
1411
+ );
1412
+ if (!response.ok) {
1413
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1414
+ throw new Error(
1415
+ errorData.error || "Failed to set default payment method"
1416
+ );
1417
+ }
1418
+ await Promise.all([refetch(), fetchPaymentMethods()]);
1419
+ return true;
1420
+ } catch (err) {
1421
+ const errorMessage = err instanceof Error ? err.message : "Failed to set default payment method";
1422
+ handleError(errorMessage);
1423
+ return false;
1424
+ }
1425
+ },
1426
+ [customerId, refetch, fetchPaymentMethods, handleError]
1427
+ );
1428
+ useEffect2(() => {
1429
+ if (autoFetch && customerId) {
1430
+ refetch();
1431
+ fetchPaymentMethods();
1432
+ }
1433
+ }, [autoFetch, customerId, refetch, fetchPaymentMethods]);
1434
+ return {
1435
+ customer,
1436
+ paymentMethods,
1437
+ loading,
1438
+ error,
1439
+ refetch,
1440
+ createCustomer,
1441
+ updateCustomer,
1442
+ deleteCustomer,
1443
+ fetchPaymentMethods,
1444
+ addPaymentMethod,
1445
+ removePaymentMethod,
1446
+ setDefaultPaymentMethod
1447
+ };
1448
+ };
1449
+
1450
+ // src/web/utils/stripe-web.ts
1451
+ import { loadStripe as loadStripe2 } from "@stripe/stripe-js";
1452
+ var stripeInstance = null;
1453
+ var getStripe2 = (publishableKey) => {
1454
+ if (!stripeInstance) {
1455
+ stripeInstance = loadStripe2(publishableKey);
1456
+ }
1457
+ return stripeInstance;
1458
+ };
1459
+ var initializeStripe = async (config) => {
1460
+ if (!config.publishableKey) {
1461
+ console.error(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1462
+ return null;
1463
+ }
1464
+ try {
1465
+ const stripe = await getStripe2(config.publishableKey);
1466
+ return stripe;
1467
+ } catch (error) {
1468
+ console.error("Failed to initialize Stripe:", error);
1469
+ return null;
1470
+ }
1471
+ };
1472
+ var createCheckoutSession = async (params) => {
1473
+ try {
1474
+ const response = await fetch("/api/payments/create-checkout-session", {
1475
+ method: "POST",
1476
+ headers: { "Content-Type": "application/json" },
1477
+ body: JSON.stringify(params)
1478
+ });
1479
+ if (!response.ok) {
1480
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1481
+ return { error: errorData.error || ERROR_MESSAGES.CHECKOUT_FAILED };
1482
+ }
1483
+ const data = await response.json();
1484
+ return { sessionId: data.sessionId };
1485
+ } catch (error) {
1486
+ return {
1487
+ error: error instanceof Error ? error.message : ERROR_MESSAGES.CHECKOUT_FAILED
1488
+ };
1489
+ }
1490
+ };
1491
+ var createPaymentIntent = async (params) => {
1492
+ try {
1493
+ const response = await fetch("/api/payments/create-payment-intent", {
1494
+ method: "POST",
1495
+ headers: { "Content-Type": "application/json" },
1496
+ body: JSON.stringify(params)
1497
+ });
1498
+ if (!response.ok) {
1499
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1500
+ return { error: errorData.error || "Failed to create payment intent" };
1501
+ }
1502
+ const data = await response.json();
1503
+ return { clientSecret: data.client_secret };
1504
+ } catch (error) {
1505
+ return {
1506
+ error: error instanceof Error ? error.message : "Failed to create payment intent"
1507
+ };
1508
+ }
1509
+ };
1510
+ var createPortalSession = async (params) => {
1511
+ try {
1512
+ const response = await fetch("/api/payments/create-portal-session", {
1513
+ method: "POST",
1514
+ headers: { "Content-Type": "application/json" },
1515
+ body: JSON.stringify(params)
1516
+ });
1517
+ if (!response.ok) {
1518
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1519
+ return { error: errorData.error || "Failed to create portal session" };
1520
+ }
1521
+ const data = await response.json();
1522
+ return { url: data.url };
1523
+ } catch (error) {
1524
+ return {
1525
+ error: error instanceof Error ? error.message : "Failed to create portal session"
1526
+ };
1527
+ }
1528
+ };
1529
+ var confirmPayment = async (stripe, elements, clientSecret, options = {}) => {
1530
+ try {
1531
+ const result = await stripe.confirmPayment({
1532
+ elements,
1533
+ clientSecret,
1534
+ confirmParams: {
1535
+ return_url: options.returnUrl || window.location.href,
1536
+ receipt_email: options.receiptEmail
1537
+ },
1538
+ redirect: "if_required"
1539
+ });
1540
+ if (result.error) {
1541
+ return { success: false, error: result.error.message };
1542
+ }
1543
+ if (result.paymentIntent?.status === "succeeded") {
1544
+ return { success: true, paymentIntent: result.paymentIntent };
1545
+ }
1546
+ return { success: false, error: "Payment was not completed" };
1547
+ } catch (error) {
1548
+ return {
1549
+ success: false,
1550
+ error: error instanceof Error ? error.message : ERROR_MESSAGES.PAYMENT_FAILED
1551
+ };
1552
+ }
1553
+ };
1554
+ var redirectToCheckout = async (stripe, sessionId) => {
1555
+ try {
1556
+ const result = await stripe.redirectToCheckout({ sessionId });
1557
+ if (result.error) {
1558
+ return { success: false, error: result.error.message };
1559
+ }
1560
+ return { success: true };
1561
+ } catch (error) {
1562
+ return {
1563
+ success: false,
1564
+ error: error instanceof Error ? error.message : ERROR_MESSAGES.CHECKOUT_FAILED
1565
+ };
1566
+ }
1567
+ };
1568
+ var formatStripeError = (error) => {
1569
+ if (typeof error === "string") {
1570
+ return error;
1571
+ }
1572
+ if (error?.message) {
1573
+ return error.message;
1574
+ }
1575
+ if (error?.code) {
1576
+ const errorMessages = {
1577
+ card_declined: "Your card was declined.",
1578
+ expired_card: "Your card has expired.",
1579
+ incorrect_cvc: "Your card's security code is incorrect.",
1580
+ processing_error: "An error occurred while processing your card.",
1581
+ incorrect_number: "Your card number is incorrect."
1582
+ };
1583
+ return errorMessages[error.code] || `Payment failed: ${error.code}`;
1584
+ }
1585
+ return "An unexpected error occurred.";
1586
+ };
1587
+ export {
1588
+ BillingPortal,
1589
+ CheckoutButton,
1590
+ PaymentForm,
1591
+ PricingTable,
1592
+ StripeProvider,
1593
+ confirmPayment,
1594
+ createCheckoutSession,
1595
+ createPaymentIntent,
1596
+ createPortalSession,
1597
+ formatStripeError,
1598
+ getStripe2 as getStripe,
1599
+ initializeStripe,
1600
+ redirectToCheckout,
1601
+ useCheckout,
1602
+ useCustomer,
1603
+ useSubscription
1604
+ };