@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,806 @@
1
+ // src/shared/constants.ts
2
+ var STRIPE_API_VERSION = "2023-10-16";
3
+ var PAYMENT_METHODS = {
4
+ CARD: "card",
5
+ BANK_ACCOUNT: "bank_account",
6
+ SEPA_DEBIT: "sepa_debit",
7
+ IDEAL: "ideal",
8
+ SOFORT: "sofort"
9
+ };
10
+ var SUBSCRIPTION_STATUS = {
11
+ ACTIVE: "active",
12
+ CANCELED: "canceled",
13
+ PAST_DUE: "past_due",
14
+ UNPAID: "unpaid",
15
+ INCOMPLETE: "incomplete",
16
+ INCOMPLETE_EXPIRED: "incomplete_expired",
17
+ TRIALING: "trialing"
18
+ };
19
+ var INVOICE_STATUS = {
20
+ DRAFT: "draft",
21
+ OPEN: "open",
22
+ PAID: "paid",
23
+ UNCOLLECTIBLE: "uncollectible",
24
+ VOID: "void"
25
+ };
26
+ var CHECKOUT_MODE = {
27
+ PAYMENT: "payment",
28
+ SUBSCRIPTION: "subscription",
29
+ SETUP: "setup"
30
+ };
31
+ var BILLING_INTERVALS = {
32
+ DAY: "day",
33
+ WEEK: "week",
34
+ MONTH: "month",
35
+ YEAR: "year"
36
+ };
37
+ var CURRENCY_SYMBOLS = {
38
+ USD: "$",
39
+ EUR: "\u20AC",
40
+ GBP: "\xA3",
41
+ JPY: "\xA5",
42
+ CAD: "C$",
43
+ AUD: "A$",
44
+ CHF: "CHF",
45
+ CNY: "\xA5",
46
+ SEK: "kr",
47
+ NZD: "NZ$"
48
+ };
49
+ var DEFAULT_CURRENCY = "USD";
50
+ var WEBHOOK_EVENTS = {
51
+ CUSTOMER_SUBSCRIPTION_CREATED: "customer.subscription.created",
52
+ CUSTOMER_SUBSCRIPTION_UPDATED: "customer.subscription.updated",
53
+ CUSTOMER_SUBSCRIPTION_DELETED: "customer.subscription.deleted",
54
+ INVOICE_PAYMENT_SUCCEEDED: "invoice.payment_succeeded",
55
+ INVOICE_PAYMENT_FAILED: "invoice.payment_failed",
56
+ CHECKOUT_SESSION_COMPLETED: "checkout.session.completed",
57
+ PAYMENT_INTENT_SUCCEEDED: "payment_intent.succeeded",
58
+ PAYMENT_INTENT_PAYMENT_FAILED: "payment_intent.payment_failed"
59
+ };
60
+ var ERROR_MESSAGES = {
61
+ PROVIDER_NOT_CONFIGURED: "Payments provider is not properly configured",
62
+ STRIPE_NOT_LOADED: "Stripe has not been loaded yet",
63
+ INVALID_PRICE_ID: "Invalid price ID provided",
64
+ INVALID_CUSTOMER_ID: "Invalid customer ID provided",
65
+ CHECKOUT_FAILED: "Checkout session creation failed",
66
+ PAYMENT_FAILED: "Payment processing failed",
67
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
68
+ CUSTOMER_NOT_FOUND: "Customer not found"
69
+ };
70
+
71
+ // src/shared/utils/validation.ts
72
+ var validateEmail = (email) => {
73
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
74
+ return emailRegex.test(email);
75
+ };
76
+ var validatePhoneNumber = (phone) => {
77
+ const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/;
78
+ return phoneRegex.test(phone);
79
+ };
80
+ var validatePaymentsConfig = (config) => {
81
+ const errors = [];
82
+ if (!config.publishableKey) {
83
+ errors.push("publishableKey is required");
84
+ }
85
+ if (!config.provider) {
86
+ errors.push("provider is required");
87
+ }
88
+ if (config.provider !== "stripe") {
89
+ errors.push("Only stripe provider is currently supported");
90
+ }
91
+ if (!config.environment) {
92
+ errors.push("environment is required");
93
+ }
94
+ if (config.environment && !["development", "production"].includes(config.environment)) {
95
+ errors.push('environment must be either "development" or "production"');
96
+ }
97
+ if (config.publishableKey && !config.publishableKey.startsWith("pk_")) {
98
+ errors.push('publishableKey must start with "pk_"');
99
+ }
100
+ if (config.secretKey && !config.secretKey.startsWith("sk_")) {
101
+ errors.push('secretKey must start with "sk_"');
102
+ }
103
+ if (config.webhookSecret && !config.webhookSecret.startsWith("whsec_")) {
104
+ errors.push('webhookSecret must start with "whsec_"');
105
+ }
106
+ return {
107
+ isValid: errors.length === 0,
108
+ errors
109
+ };
110
+ };
111
+ var validatePricingPlan = (plan) => {
112
+ const errors = [];
113
+ if (!plan.id) {
114
+ errors.push("id is required");
115
+ }
116
+ if (!plan.name) {
117
+ errors.push("name is required");
118
+ }
119
+ if (typeof plan.price !== "number" || plan.price < 0) {
120
+ errors.push("price must be a non-negative number");
121
+ }
122
+ if (!plan.currency) {
123
+ errors.push("currency is required");
124
+ }
125
+ if (!plan.interval) {
126
+ errors.push("interval is required");
127
+ }
128
+ if (!["day", "week", "month", "year"].includes(plan.interval)) {
129
+ errors.push("interval must be one of: day, week, month, year");
130
+ }
131
+ if (!plan.stripePriceId) {
132
+ errors.push("stripePriceId is required");
133
+ }
134
+ if (plan.stripePriceId && !plan.stripePriceId.startsWith("price_")) {
135
+ errors.push('stripePriceId must start with "price_"');
136
+ }
137
+ if (!Array.isArray(plan.features)) {
138
+ errors.push("features must be an array");
139
+ }
140
+ if (plan.intervalCount && (typeof plan.intervalCount !== "number" || plan.intervalCount < 1)) {
141
+ errors.push("intervalCount must be a positive number");
142
+ }
143
+ if (plan.trialPeriodDays && (typeof plan.trialPeriodDays !== "number" || plan.trialPeriodDays < 0)) {
144
+ errors.push("trialPeriodDays must be a non-negative number");
145
+ }
146
+ return {
147
+ isValid: errors.length === 0,
148
+ errors
149
+ };
150
+ };
151
+ var validateStripeId = (id, type) => {
152
+ const prefixes = {
153
+ customer: "cus_",
154
+ subscription: "sub_",
155
+ price: "price_",
156
+ product: "prod_",
157
+ payment_intent: "pi_"
158
+ };
159
+ return id.startsWith(prefixes[type]);
160
+ };
161
+ var validateAmount = (amount, currency) => {
162
+ if (typeof amount !== "number") {
163
+ return { isValid: false, error: "Amount must be a number" };
164
+ }
165
+ if (amount < 0) {
166
+ return { isValid: false, error: "Amount must be non-negative" };
167
+ }
168
+ const minimumAmounts = {
169
+ USD: 50,
170
+ // $0.50
171
+ EUR: 50,
172
+ // €0.50
173
+ GBP: 30,
174
+ // £0.30
175
+ JPY: 50,
176
+ // ¥50
177
+ CAD: 50,
178
+ // C$0.50
179
+ AUD: 50
180
+ // A$0.50
181
+ };
182
+ const minAmount = minimumAmounts[currency.toUpperCase()] || 50;
183
+ if (amount < minAmount) {
184
+ return {
185
+ isValid: false,
186
+ error: `Amount must be at least ${minAmount} ${currency.toLowerCase()} cents`
187
+ };
188
+ }
189
+ return { isValid: true };
190
+ };
191
+
192
+ // src/shared/utils/formatting.ts
193
+ var formatCurrency = (amount, currency = DEFAULT_CURRENCY, options = {}) => {
194
+ const { showSymbol = true, showCents = true, locale = "en-US" } = options;
195
+ const currencyCode = currency.toUpperCase();
196
+ const isZeroDecimalCurrency = ["JPY", "KRW", "VND", "CLP"].includes(
197
+ currencyCode
198
+ );
199
+ const displayAmount = isZeroDecimalCurrency ? amount : amount / 100;
200
+ if (showSymbol) {
201
+ try {
202
+ return new Intl.NumberFormat(locale, {
203
+ style: "currency",
204
+ currency: currencyCode,
205
+ minimumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0,
206
+ maximumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0
207
+ }).format(displayAmount);
208
+ } catch (error) {
209
+ const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
210
+ const formattedAmount = showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
211
+ return `${symbol}${formattedAmount}`;
212
+ }
213
+ }
214
+ return showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
215
+ };
216
+ var formatPricingPlan = (plan) => {
217
+ const price = formatCurrency(plan.price, plan.currency);
218
+ const interval = plan.intervalCount && plan.intervalCount > 1 ? `${plan.intervalCount} ${plan.interval}s` : plan.interval;
219
+ return `${price}/${interval}`;
220
+ };
221
+ var formatSubscriptionStatus = (status) => {
222
+ const statusMap = {
223
+ active: "Active",
224
+ canceled: "Canceled",
225
+ past_due: "Past Due",
226
+ unpaid: "Unpaid",
227
+ incomplete: "Incomplete",
228
+ incomplete_expired: "Incomplete (Expired)",
229
+ trialing: "Trial"
230
+ };
231
+ return statusMap[status] || status;
232
+ };
233
+ var formatDate = (date, options = {}) => {
234
+ const { format = "medium", locale = "en-US", timeZone } = options;
235
+ const dateObj = typeof date === "string" || typeof date === "number" ? new Date(date) : date;
236
+ const formatOptions = {
237
+ timeZone
238
+ };
239
+ switch (format) {
240
+ case "short":
241
+ formatOptions.dateStyle = "short";
242
+ break;
243
+ case "medium":
244
+ formatOptions.dateStyle = "medium";
245
+ break;
246
+ case "long":
247
+ formatOptions.dateStyle = "long";
248
+ break;
249
+ case "full":
250
+ formatOptions.dateStyle = "full";
251
+ break;
252
+ }
253
+ try {
254
+ return new Intl.DateTimeFormat(locale, formatOptions).format(dateObj);
255
+ } catch (error) {
256
+ return dateObj.toLocaleDateString();
257
+ }
258
+ };
259
+ var formatRelativeTime = (date, options = {}) => {
260
+ const { locale = "en-US", numeric = "auto" } = options;
261
+ const dateObj = typeof date === "string" || typeof date === "number" ? new Date(date) : date;
262
+ const now = /* @__PURE__ */ new Date();
263
+ const diffInSeconds = Math.floor((now.getTime() - dateObj.getTime()) / 1e3);
264
+ try {
265
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric });
266
+ if (Math.abs(diffInSeconds) < 60) {
267
+ return rtf.format(-diffInSeconds, "second");
268
+ } else if (Math.abs(diffInSeconds) < 3600) {
269
+ return rtf.format(-Math.floor(diffInSeconds / 60), "minute");
270
+ } else if (Math.abs(diffInSeconds) < 86400) {
271
+ return rtf.format(-Math.floor(diffInSeconds / 3600), "hour");
272
+ } else if (Math.abs(diffInSeconds) < 2592e3) {
273
+ return rtf.format(-Math.floor(diffInSeconds / 86400), "day");
274
+ } else if (Math.abs(diffInSeconds) < 31536e3) {
275
+ return rtf.format(-Math.floor(diffInSeconds / 2592e3), "month");
276
+ } else {
277
+ return rtf.format(-Math.floor(diffInSeconds / 31536e3), "year");
278
+ }
279
+ } catch (error) {
280
+ if (Math.abs(diffInSeconds) < 60) {
281
+ return "just now";
282
+ } else if (Math.abs(diffInSeconds) < 3600) {
283
+ const minutes = Math.floor(Math.abs(diffInSeconds) / 60);
284
+ return diffInSeconds < 0 ? `in ${minutes} minutes` : `${minutes} minutes ago`;
285
+ } else if (Math.abs(diffInSeconds) < 86400) {
286
+ const hours = Math.floor(Math.abs(diffInSeconds) / 3600);
287
+ return diffInSeconds < 0 ? `in ${hours} hours` : `${hours} hours ago`;
288
+ } else {
289
+ const days = Math.floor(Math.abs(diffInSeconds) / 86400);
290
+ return diffInSeconds < 0 ? `in ${days} days` : `${days} days ago`;
291
+ }
292
+ }
293
+ };
294
+ var formatBillingInterval = (interval, count = 1) => {
295
+ const intervalMap = {
296
+ day: count === 1 ? "daily" : `every ${count} days`,
297
+ week: count === 1 ? "weekly" : `every ${count} weeks`,
298
+ month: count === 1 ? "monthly" : `every ${count} months`,
299
+ year: count === 1 ? "yearly" : `every ${count} years`
300
+ };
301
+ return intervalMap[interval] || interval;
302
+ };
303
+ var formatTrialPeriod = (days) => {
304
+ if (days === 0) return "No trial";
305
+ if (days === 1) return "1 day trial";
306
+ if (days < 7) return `${days} days trial`;
307
+ if (days === 7) return "1 week trial";
308
+ if (days < 30) return `${Math.floor(days / 7)} weeks trial`;
309
+ if (days === 30) return "1 month trial";
310
+ return `${Math.floor(days / 30)} months trial`;
311
+ };
312
+ var truncateText = (text, maxLength) => {
313
+ if (text.length <= maxLength) return text;
314
+ return `${text.substring(0, maxLength - 3)}...`;
315
+ };
316
+ var formatCardBrand = (brand) => {
317
+ const brandMap = {
318
+ visa: "Visa",
319
+ mastercard: "Mastercard",
320
+ amex: "American Express",
321
+ discover: "Discover",
322
+ jcb: "JCB",
323
+ diners: "Diners Club",
324
+ unionpay: "UnionPay"
325
+ };
326
+ return brandMap[brand.toLowerCase()] || brand;
327
+ };
328
+ var formatPaymentMethodDisplay = (paymentMethod) => {
329
+ if (paymentMethod.type === "card" && paymentMethod.card) {
330
+ return `${formatCardBrand(paymentMethod.card.brand)} \u2022\u2022\u2022\u2022 ${paymentMethod.card.last4}`;
331
+ }
332
+ if (paymentMethod.type === "bank_account" && paymentMethod.bankAccount) {
333
+ const bankName = paymentMethod.bankAccount.bankName || "Bank";
334
+ return `${bankName} \u2022\u2022\u2022\u2022 ${paymentMethod.bankAccount.last4}`;
335
+ }
336
+ return paymentMethod.type;
337
+ };
338
+
339
+ // src/shared/utils/env.ts
340
+ var validateEnvironment = (config) => {
341
+ const errors = [];
342
+ const warnings = [];
343
+ if (!config.stripePublishableKey) {
344
+ errors.push("STRIPE_PUBLISHABLE_KEY is required");
345
+ } else if (!config.stripePublishableKey.startsWith("pk_")) {
346
+ errors.push('STRIPE_PUBLISHABLE_KEY must start with "pk_"');
347
+ }
348
+ if (!config.environment) {
349
+ errors.push("Environment must be specified (development or production)");
350
+ } else if (!["development", "production"].includes(config.environment)) {
351
+ errors.push('Environment must be either "development" or "production"');
352
+ }
353
+ if (config.environment === "production") {
354
+ if (config.stripePublishableKey?.includes("test")) {
355
+ warnings.push("Using test keys in production environment");
356
+ }
357
+ if (!config.appUrl) {
358
+ warnings.push("APP_URL should be set in production for proper redirects");
359
+ } else if (!config.appUrl.startsWith("https://")) {
360
+ warnings.push("APP_URL should use HTTPS in production");
361
+ }
362
+ }
363
+ if (typeof window === "undefined") {
364
+ if (!config.stripeSecretKey) {
365
+ warnings.push(
366
+ "STRIPE_SECRET_KEY is recommended for server-side operations"
367
+ );
368
+ } else if (!config.stripeSecretKey.startsWith("sk_")) {
369
+ errors.push('STRIPE_SECRET_KEY must start with "sk_"');
370
+ }
371
+ if (!config.stripeWebhookSecret) {
372
+ warnings.push(
373
+ "STRIPE_WEBHOOK_SECRET is recommended for webhook verification"
374
+ );
375
+ } else if (!config.stripeWebhookSecret.startsWith("whsec_")) {
376
+ errors.push('STRIPE_WEBHOOK_SECRET must start with "whsec_"');
377
+ }
378
+ }
379
+ return {
380
+ isValid: errors.length === 0,
381
+ errors,
382
+ warnings
383
+ };
384
+ };
385
+ var getEnvironmentConfig = () => {
386
+ const isServer2 = typeof window === "undefined";
387
+ const config = {
388
+ stripePublishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || "",
389
+ environment: process.env.NODE_ENV || "development",
390
+ appUrl: process.env.NEXT_PUBLIC_APP_URL
391
+ };
392
+ if (isServer2) {
393
+ config.stripeSecretKey = process.env.STRIPE_SECRET_KEY;
394
+ config.stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
395
+ }
396
+ return config;
397
+ };
398
+ var validateCurrentEnvironment = () => {
399
+ const config = getEnvironmentConfig();
400
+ const validation = validateEnvironment(config);
401
+ if (!validation.isValid) {
402
+ console.error("\u274C Environment validation failed:");
403
+ validation.errors.forEach((error) => console.error(` - ${error}`));
404
+ if (typeof window === "undefined") {
405
+ throw new Error(
406
+ `Environment validation failed: ${validation.errors.join(", ")}`
407
+ );
408
+ }
409
+ }
410
+ if (validation.warnings.length > 0) {
411
+ console.warn("\u26A0\uFE0F Environment warnings:");
412
+ validation.warnings.forEach((warning) => console.warn(` - ${warning}`));
413
+ }
414
+ return validation;
415
+ };
416
+ var isDevelopment = () => {
417
+ return process.env.NODE_ENV === "development";
418
+ };
419
+ var isProduction = () => {
420
+ return process.env.NODE_ENV === "production";
421
+ };
422
+ var isServer = () => {
423
+ return typeof window === "undefined";
424
+ };
425
+ var isClient = () => {
426
+ return typeof window !== "undefined";
427
+ };
428
+
429
+ // src/shared/providers/PaymentsProvider.tsx
430
+ import { createContext, useContext } from "react";
431
+ import { jsx } from "react/jsx-runtime";
432
+ var PaymentsContext = createContext(
433
+ void 0
434
+ );
435
+ var PaymentsProvider = ({
436
+ children,
437
+ config
438
+ }) => {
439
+ const isConfigured = Boolean(
440
+ config.publishableKey && config.provider === "stripe" && config.environment
441
+ );
442
+ const isProduction2 = config.environment === "production";
443
+ if (!isConfigured && config.environment === "development") {
444
+ console.warn(
445
+ "PaymentsProvider: Configuration incomplete. Please ensure publishableKey and provider are set."
446
+ );
447
+ }
448
+ const contextValue = {
449
+ config,
450
+ isConfigured,
451
+ isProduction: isProduction2
452
+ };
453
+ return /* @__PURE__ */ jsx(PaymentsContext.Provider, { value: contextValue, children });
454
+ };
455
+ var usePaymentsConfig = () => {
456
+ const context = useContext(PaymentsContext);
457
+ if (!context) {
458
+ throw new Error("usePaymentsConfig must be used within PaymentsProvider");
459
+ }
460
+ return context;
461
+ };
462
+ var usePaymentsConfigSafe = () => {
463
+ return useContext(PaymentsContext) || null;
464
+ };
465
+
466
+ // src/shared/hooks/usePayments.ts
467
+ import { useState, useEffect, useCallback } from "react";
468
+ var usePayments = (options = {}) => {
469
+ const { customerId, autoFetch = true, onError } = options;
470
+ const { isConfigured } = usePaymentsConfig();
471
+ const [customer, setCustomer] = useState(null);
472
+ const [subscriptions, setSubscriptions] = useState([]);
473
+ const [loading, setLoading] = useState(false);
474
+ const [error, setError] = useState(null);
475
+ const handleError = useCallback(
476
+ (errorMessage) => {
477
+ setError(errorMessage);
478
+ onError?.(errorMessage);
479
+ console.error("Payments error:", errorMessage);
480
+ },
481
+ [onError]
482
+ );
483
+ const fetchCustomerData = useCallback(
484
+ async (id) => {
485
+ if (!validateStripeId(id, "customer")) {
486
+ handleError("Invalid customer ID");
487
+ return;
488
+ }
489
+ try {
490
+ const response = await fetch(`/api/payments/customer/${id}`);
491
+ if (!response.ok) {
492
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
493
+ throw new Error(errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND);
494
+ }
495
+ const customerData = await response.json();
496
+ setCustomer(customerData);
497
+ const subsResponse = await fetch(
498
+ `/api/payments/customer/${id}/subscriptions`
499
+ );
500
+ if (subsResponse.ok) {
501
+ const subsData = await subsResponse.json();
502
+ setSubscriptions(subsData.subscriptions || []);
503
+ }
504
+ } catch (err) {
505
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND;
506
+ handleError(errorMessage);
507
+ }
508
+ },
509
+ [handleError]
510
+ );
511
+ const refetch = useCallback(async () => {
512
+ if (!customerId) {
513
+ handleError("Customer ID is required");
514
+ return;
515
+ }
516
+ if (!isConfigured) {
517
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
518
+ return;
519
+ }
520
+ setLoading(true);
521
+ setError(null);
522
+ try {
523
+ await fetchCustomerData(customerId);
524
+ } finally {
525
+ setLoading(false);
526
+ }
527
+ }, [customerId, isConfigured, fetchCustomerData, handleError]);
528
+ const createCustomer = useCallback(
529
+ async (params) => {
530
+ if (!isConfigured) {
531
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
532
+ return null;
533
+ }
534
+ if (!validateEmail(params.email)) {
535
+ handleError("Invalid email address");
536
+ return null;
537
+ }
538
+ setLoading(true);
539
+ setError(null);
540
+ try {
541
+ const response = await fetch("/api/payments/customer", {
542
+ method: "POST",
543
+ headers: { "Content-Type": "application/json" },
544
+ body: JSON.stringify(params)
545
+ });
546
+ if (!response.ok) {
547
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
548
+ throw new Error(errorData.error || "Failed to create customer");
549
+ }
550
+ const customerData = await response.json();
551
+ setCustomer(customerData);
552
+ return customerData;
553
+ } catch (err) {
554
+ const errorMessage = err instanceof Error ? err.message : "Failed to create customer";
555
+ handleError(errorMessage);
556
+ return null;
557
+ } finally {
558
+ setLoading(false);
559
+ }
560
+ },
561
+ [isConfigured, handleError]
562
+ );
563
+ const updateCustomer = useCallback(
564
+ async (params) => {
565
+ if (!customerId) {
566
+ handleError("Customer ID is required");
567
+ return false;
568
+ }
569
+ if (!isConfigured) {
570
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
571
+ return false;
572
+ }
573
+ if (params.email && !validateEmail(params.email)) {
574
+ handleError("Invalid email address");
575
+ return false;
576
+ }
577
+ setLoading(true);
578
+ setError(null);
579
+ try {
580
+ const response = await fetch(`/api/payments/customer/${customerId}`, {
581
+ method: "PUT",
582
+ headers: { "Content-Type": "application/json" },
583
+ body: JSON.stringify(params)
584
+ });
585
+ if (!response.ok) {
586
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
587
+ throw new Error(errorData.error || "Failed to update customer");
588
+ }
589
+ await refetch();
590
+ return true;
591
+ } catch (err) {
592
+ const errorMessage = err instanceof Error ? err.message : "Failed to update customer";
593
+ handleError(errorMessage);
594
+ return false;
595
+ } finally {
596
+ setLoading(false);
597
+ }
598
+ },
599
+ [customerId, isConfigured, refetch, handleError]
600
+ );
601
+ const subscribe = useCallback(
602
+ async (priceId, options2 = {}) => {
603
+ if (!customerId) {
604
+ handleError("Customer ID is required");
605
+ return false;
606
+ }
607
+ if (!isConfigured) {
608
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
609
+ return false;
610
+ }
611
+ if (!validateStripeId(priceId, "price")) {
612
+ handleError(ERROR_MESSAGES.INVALID_PRICE_ID);
613
+ return false;
614
+ }
615
+ setLoading(true);
616
+ setError(null);
617
+ try {
618
+ const response = await fetch("/api/payments/create-subscription", {
619
+ method: "POST",
620
+ headers: { "Content-Type": "application/json" },
621
+ body: JSON.stringify({
622
+ customerId,
623
+ priceId,
624
+ ...options2
625
+ })
626
+ });
627
+ if (!response.ok) {
628
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
629
+ throw new Error(errorData.error || "Failed to create subscription");
630
+ }
631
+ await refetch();
632
+ return true;
633
+ } catch (err) {
634
+ const errorMessage = err instanceof Error ? err.message : "Failed to create subscription";
635
+ handleError(errorMessage);
636
+ return false;
637
+ } finally {
638
+ setLoading(false);
639
+ }
640
+ },
641
+ [customerId, isConfigured, refetch, handleError]
642
+ );
643
+ const cancelSubscription = useCallback(
644
+ async (subscriptionId) => {
645
+ const activeSubscription2 = subscriptions.find((s) => s.status === "active");
646
+ const idToCancel = subscriptionId || activeSubscription2?.id;
647
+ if (!idToCancel) {
648
+ handleError("No active subscription to cancel");
649
+ return false;
650
+ }
651
+ if (!validateStripeId(idToCancel, "subscription")) {
652
+ handleError("Invalid subscription ID");
653
+ return false;
654
+ }
655
+ try {
656
+ const response = await fetch(
657
+ `/api/payments/subscription/${idToCancel}/cancel`,
658
+ {
659
+ method: "POST",
660
+ headers: { "Content-Type": "application/json" }
661
+ }
662
+ );
663
+ if (!response.ok) {
664
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
665
+ throw new Error(errorData.error || "Failed to cancel subscription");
666
+ }
667
+ await refetch();
668
+ return true;
669
+ } catch (err) {
670
+ const errorMessage = err instanceof Error ? err.message : "Failed to cancel subscription";
671
+ handleError(errorMessage);
672
+ return false;
673
+ }
674
+ },
675
+ [subscriptions, refetch, handleError]
676
+ );
677
+ const reactivateSubscription = useCallback(
678
+ async (subscriptionId) => {
679
+ const cancelledSubscription = subscriptions.find(
680
+ (s) => s.cancelAtPeriodEnd
681
+ );
682
+ const idToReactivate = subscriptionId || cancelledSubscription?.id;
683
+ if (!idToReactivate) {
684
+ handleError("No cancelled subscription to reactivate");
685
+ return false;
686
+ }
687
+ if (!validateStripeId(idToReactivate, "subscription")) {
688
+ handleError("Invalid subscription ID");
689
+ return false;
690
+ }
691
+ try {
692
+ const response = await fetch(
693
+ `/api/payments/subscription/${idToReactivate}/reactivate`,
694
+ {
695
+ method: "POST",
696
+ headers: { "Content-Type": "application/json" }
697
+ }
698
+ );
699
+ if (!response.ok) {
700
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
701
+ throw new Error(
702
+ errorData.error || "Failed to reactivate subscription"
703
+ );
704
+ }
705
+ await refetch();
706
+ return true;
707
+ } catch (err) {
708
+ const errorMessage = err instanceof Error ? err.message : "Failed to reactivate subscription";
709
+ handleError(errorMessage);
710
+ return false;
711
+ }
712
+ },
713
+ [subscriptions, refetch, handleError]
714
+ );
715
+ const hasActiveSubscription = useCallback(() => {
716
+ return subscriptions.some(
717
+ (s) => s.status === "active" || s.status === "trialing"
718
+ );
719
+ }, [subscriptions]);
720
+ const isSubscribedToPlan = useCallback(
721
+ (priceId) => {
722
+ return subscriptions.some(
723
+ (s) => s.priceId === priceId && (s.status === "active" || s.status === "trialing")
724
+ );
725
+ },
726
+ [subscriptions]
727
+ );
728
+ const getSubscriptionStatus = useCallback(
729
+ (subscriptionId) => {
730
+ if (subscriptionId) {
731
+ const subscription = subscriptions.find((s) => s.id === subscriptionId);
732
+ return subscription?.status || null;
733
+ }
734
+ const activeSubscription2 = subscriptions.find(
735
+ (s) => s.status === "active" || s.status === "trialing"
736
+ );
737
+ return activeSubscription2?.status || null;
738
+ },
739
+ [subscriptions]
740
+ );
741
+ const activeSubscription = subscriptions.find((s) => s.status === "active" || s.status === "trialing") || null;
742
+ useEffect(() => {
743
+ if (autoFetch && customerId && isConfigured) {
744
+ refetch();
745
+ }
746
+ }, [autoFetch, customerId, isConfigured, refetch]);
747
+ return {
748
+ // State
749
+ customer,
750
+ subscriptions,
751
+ activeSubscription,
752
+ loading,
753
+ error,
754
+ // Customer methods
755
+ createCustomer,
756
+ updateCustomer,
757
+ // Subscription methods
758
+ subscribe,
759
+ cancelSubscription,
760
+ reactivateSubscription,
761
+ // Utility methods
762
+ refetch,
763
+ hasActiveSubscription,
764
+ isSubscribedToPlan,
765
+ getSubscriptionStatus
766
+ };
767
+ };
768
+ export {
769
+ BILLING_INTERVALS,
770
+ CHECKOUT_MODE,
771
+ CURRENCY_SYMBOLS,
772
+ DEFAULT_CURRENCY,
773
+ ERROR_MESSAGES,
774
+ INVOICE_STATUS,
775
+ PAYMENT_METHODS,
776
+ PaymentsProvider,
777
+ STRIPE_API_VERSION,
778
+ SUBSCRIPTION_STATUS,
779
+ WEBHOOK_EVENTS,
780
+ formatBillingInterval,
781
+ formatCardBrand,
782
+ formatCurrency,
783
+ formatDate,
784
+ formatPaymentMethodDisplay,
785
+ formatPricingPlan,
786
+ formatRelativeTime,
787
+ formatSubscriptionStatus,
788
+ formatTrialPeriod,
789
+ getEnvironmentConfig,
790
+ isClient,
791
+ isDevelopment,
792
+ isProduction,
793
+ isServer,
794
+ truncateText,
795
+ usePayments,
796
+ usePaymentsConfig,
797
+ usePaymentsConfigSafe,
798
+ validateAmount,
799
+ validateCurrentEnvironment,
800
+ validateEmail,
801
+ validateEnvironment,
802
+ validatePaymentsConfig,
803
+ validatePhoneNumber,
804
+ validatePricingPlan,
805
+ validateStripeId
806
+ };